Dialog
Example
Simple Modal Dialog Example
Help Section
Here you can find help and some frequently asked questions about our service.
Confirmation Dialog Example
Confirm Deletion
Are you sure you want to delete this item? This action cannot be undone.
Code
import cn from '@/utils/cn'
import { applyPropsToChildrenOfType } from '@/utils/manipulateReactComponents'
import { cva, type VariantProps } from 'class-variance-authority'
import React, { forwardRef, useId, type CSSProperties } from 'react'
import { HiOutlineXMark } from 'react-icons/hi2'
import { DialogTrigger as BaseDialogTrigger, DialogState, DialogTriggerProps } from './client'
export type DialogProps = VariantProps<typeof dialog> & {
hideCloseButton?: boolean
customState?: boolean
blockBodyScroll?: boolean
closeOnBackdrop?: boolean
wrapperClass?: string
className?: string
closeOnEscape?: boolean
closeButton?: React.ReactNode
transitionDuration?: number
children: React.ReactNode
backdrop?: boolean
defaultOpen?: boolean
id: string
style?: CSSProperties
ariaLabel?: string
}
const dialog = cva(
[
'group modal isolate fixed bg-transparent z-[999] p-0 outline-none invisible scale-0 opacity-0 w-fit h-fit max-w-[100vw] max-h-dvh',
'peer-checked:scale-100 peer-checked:opacity-100 peer-checked:visible'
],
{
variants: {
noInTransition: {
true: 'peer-checked:!duration-0'
},
noOutTransition: {
true: '[.peer:not(:checked)_~_&]:!duration-0'
},
alignX: {
center: 'mx-auto left-0 right-0',
start: 'mr-auto ml-0 left-0',
end: 'ml-auto mr-0 right-0'
},
alignY: {
center: 'my-auto top-0 bottom-0',
start: 'mb-auto mt-0 top-0',
end: 'mt-auto mb-0 bottom-0'
},
animationStyle: {
scaleFade: 'scale-95 opacity-0 transition-all duration-[var(--animation-duration)] ease-in-out',
scale: 'scale-95 transition-all duration-[var(--animation-duration)] ease-in-out',
fade: 'opacity-0 transition-all duration-[var(--animation-duration)] ease-in-out',
none: ''
}
},
compoundVariants: [
{ alignX: 'center', alignY: 'center', class: 'origin-center' },
{ alignX: 'start', alignY: 'start', class: 'origin-top-left' },
{ alignX: 'start', alignY: 'center', class: 'origin-left' },
{ alignX: 'start', alignY: 'end', class: 'origin-bottom-left' },
{ alignX: 'center', alignY: 'start', class: 'origin-top' },
{ alignX: 'center', alignY: 'end', class: 'origin-bottom' },
{ alignX: 'end', alignY: 'start', class: 'origin-top-right' },
{ alignX: 'end', alignY: 'center', class: 'origin-right' },
{ alignX: 'end', alignY: 'end', class: 'origin-bottom-right' }
],
defaultVariants: {
noInTransition: false,
noOutTransition: false,
alignX: 'center',
alignY: 'center',
animationStyle: 'scaleFade'
}
}
)
const Dialog: React.FC<DialogProps> = ({ wrapperClass, customState = false, className, style, children, ...props }) => {
const fallbackId = useId()
const { noInTransition = false, noOutTransition = false, transitionDuration = 300, closeOnBackdrop = true, backdrop = true } = props
const id = props.id ?? fallbackId
const childrenWithExtraProps = applyPropsToChildrenOfType(children, { ...props, id, className }, [DialogContent])
return (
<div
className="relative z-[999]"
style={
{
'--animation-duration': `${transitionDuration}ms`,
...style
} as React.CSSProperties
}>
{!customState ? <DialogState {...props} /> : null}
{childrenWithExtraProps}
{closeOnBackdrop || backdrop ? <DialogBackdrop {...{ id, closeOnBackdrop, noInTransition, noOutTransition }} /> : null}
</div>
)
}
Dialog.displayName = 'Dialog'
const DialogContent: React.FC<
Omit<DialogProps, 'id'> & {
id?: string
}
> = ({ closeOnBackdrop, backdrop, noInTransition, noOutTransition, alignX, alignY, children, animationStyle, wrapperClass, className, ariaLabel }) => {
return (
<div
role="dialog"
aria-label={ariaLabel}
aria-modal={closeOnBackdrop || backdrop}
className={cn(
dialog({
noInTransition,
noOutTransition,
alignX,
alignY,
animationStyle
}),
wrapperClass
)}>
<div
className={cn(
'dialog-content relative z-[1] m-4 h-fit max-h-dvh w-fit max-w-[100dvw] overflow-y-auto rounded-lg p-4 dark:border-zinc-700 dark:bg-zinc-800 dark:text-white',
className
)}>
{children}
</div>
</div>
)
}
DialogContent.displayName = 'DialogContent'
const DialogBackdrop: React.FC<{
closeOnBackdrop?: DialogProps['closeOnBackdrop']
noInTransition: DialogProps['noInTransition']
noOutTransition: DialogProps['noOutTransition']
className?: string
id: string
}> = ({ id, closeOnBackdrop, className, noInTransition, noOutTransition }) => {
const Component = closeOnBackdrop ? 'label' : 'div'
const componentProps: Record<string, string> = {
className: cn(
'modal-backdrop fixed inset-0 min-h-[100vh] z-[0] min-w-[100vw] overflow-y-hidden text-transparent outline-none',
'peer-checked:visible peer-checked:opacity-100 bg-black/50 opacity-0 invisible transition-all duration-[--animation-duration] ease-in-out',
closeOnBackdrop && 'cursor-pointer',
noInTransition && 'peer-checked:duration-0',
noOutTransition && '[.peer:not(:checked)_~_&]:duration-0',
className
)
}
if (closeOnBackdrop) componentProps.htmlFor = id
return React.createElement(Component, componentProps, null)
}
DialogBackdrop.displayName = 'DialogBackdrop'
const DialogClose = forwardRef<
HTMLLabelElement,
React.ComponentPropsWithRef<'label'> & {
id: string
asChild?: React.ElementType
}
>(({ asChild: Component = 'label', id, children, ...props }, ref) => {
return (
<Component ref={ref} htmlFor={id} aria-label="Close" {...props}>
{children}
</Component>
)
})
DialogClose.displayName = 'DialogClose'
const DialogXClose = React.forwardRef<HTMLLabelElement, React.ComponentPropsWithRef<typeof DialogClose> & { children?: React.ReactNode }>(
({ className, children = <HiOutlineXMark className="h-6 w-6" />, ...props }, ref) => (
<DialogClose
ref={ref}
className={cn(
'absolute right-2 top-2 inline-flex h-9 w-9 cursor-pointer items-center justify-center rounded-full !outline-none !ring-0 transition-colors duration-300 ease-in-out dark:bg-zinc-800 dark:text-white dark:hover:bg-zinc-700',
className
)}
{...props}>
{children}
</DialogClose>
)
)
DialogXClose.displayName = 'DialogXClose'
const DialogTrigger = React.forwardRef<HTMLLabelElement, DialogTriggerProps>(({ className, ...props }, ref) => (
<BaseDialogTrigger ref={ref} className={cn('inline-flex cursor-pointer rounded-md border border-zinc-700 bg-zinc-800 px-5 py-1.5 !outline-none !ring-0', className)} {...props} />
))
DialogTrigger.displayName = 'DialogTrigger'
export { Dialog, DialogBackdrop, DialogClose, DialogContent, DialogState, DialogTrigger, DialogXClose }
'use client'
import React, { forwardRef, useEffect, useRef, useState } from 'react'
interface DialogStateProps {
id: string
isOpen?: boolean
defaultOpen?: boolean
blockBodyScroll?: boolean
onClose?: () => void
closeOnEscape?: boolean
onEscPress?: () => void
ariaLabel?: string
}
const DialogState = forwardRef<HTMLInputElement, DialogStateProps>(
({ id, isOpen, defaultOpen = false, blockBodyScroll = false, onClose, closeOnEscape = true, onEscPress = () => {}, ariaLabel }, ref) => {
const [checked, setChecked] = useState<boolean>(isOpen ?? defaultOpen)
useEffect(() => {
if (isOpen !== undefined) {
setChecked(isOpen)
}
}, [isOpen])
useEffect(() => {
if (checked && blockBodyScroll) {
document.body.style.setProperty('overflow', 'hidden')
} else {
document.body.style.removeProperty('overflow')
}
}, [checked, blockBodyScroll])
useEffect(() => {
const handleKeyPress = (e: KeyboardEvent) => {
if (e.key === 'Escape' && closeOnEscape) {
if (checked) {
setChecked(false)
onEscPress()
onClose && onClose()
}
}
}
document.addEventListener('keydown', handleKeyPress)
return () => {
document.removeEventListener('keydown', handleKeyPress)
}
}, [closeOnEscape, onClose, checked, onEscPress])
const handleChange = (e: any) => {
const newChecked = e.target.checked
setChecked(e.target.checked)
if (!newChecked) onClose && onClose()
}
return <input ref={ref} type="checkbox" aria-label={`Toggle ${ariaLabel}`} id={id} checked={checked} onChange={handleChange} className="peer hidden" />
}
)
DialogState.displayName = 'DialogState'
export type DialogTriggerProps = {
children: React.ReactNode
id?: string
className?: string
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void
}
const DialogTrigger = React.forwardRef<HTMLLabelElement, DialogTriggerProps>(({ children, id, className, onClick }, ref) => {
return (
<label
ref={ref}
htmlFor={id}
aria-haspopup="dialog"
onClick={(e: any) => {
onClick && onClick(e)
}}
className={className}>
{children}
</label>
)
})
DialogTrigger.displayName = 'DialogTrigger'
export { DialogTrigger, DialogState }