1'use client';23import { Button } from '@/components/ui/button';4import { Dialog } from '@/components/ui/dialog';56export function Default() {7 return (8 <Dialog>9 <Dialog.Trigger asChild>10 <Button>Open Dialog</Button>11 </Dialog.Trigger>12 <Dialog.Content>13 <Dialog.Header>14 <Dialog.Title>Are you absolutely sure?</Dialog.Title>15 <Dialog.Description>16 This action cannot be undone. This will permanently delete your account and remove your17 data from our servers.18 </Dialog.Description>19 </Dialog.Header>20 <Dialog.Footer>21 <Dialog.Close asChild>22 <Button variant="outline">Cancel</Button>23 </Dialog.Close>24 <Dialog.Close asChild>25 <Button variant="destructive">Delete Account</Button>26 </Dialog.Close>27 </Dialog.Footer>28 </Dialog.Content>29 </Dialog>30 );31}
Installation
Copy and paste the following code in your project.
'use client';import { Cancel01Icon } from '@hugeicons/core-free-icons';import { HugeiconsIcon } from '@hugeicons/react';import { AnimatePresence, HTMLMotionProps, motion } from 'motion/react';import * as React from 'react';import { cloneElement } from 'react';import { createPortal } from 'react-dom';import { cn } from '../lib/cn';// --- Animation constants (module level) ---const OVERLAY_VARIANTS = {initial: { opacity: 0, backdropFilter: 'blur(0px)' },animate: { opacity: 1, backdropFilter: 'blur(4px)' },exit: { opacity: 0, backdropFilter: 'blur(0px)' },} as const;const OVERLAY_TRANSITION = { duration: 0.3 } as const;const OVERLAY_STYLE = { willChange: 'opacity' } as const;const DIALOG_VARIANTS = {initial: { opacity: 0, scale: 0.9, filter: 'blur(10px)', y: 10 },animate: { opacity: 1, scale: 1, filter: 'blur(0px)', y: 0 },exit: { opacity: 0, scale: 0.9, filter: 'blur(10px)', y: 10 },} as const;const DIALOG_TRANSITION = {type: 'spring',damping: 25,stiffness: 350,mass: 0.5,} as const;const DIALOG_EXIT_TRANSITION = { duration: 0.2, ease: 'easeIn' } as const;const DIALOG_STYLE = { willChange: 'opacity, transform, filter' } as const;// --- Context ---type DialogContextType = {open: boolean;setOpen: React.Dispatch<React.SetStateAction<boolean>>;id: string;};const DialogContext = React.createContext<DialogContextType | undefined>(undefined);const useDialogContext = () => {const context = React.use(DialogContext);if (!context) {throw new Error('useDialogContext must be used within a Dialog');}return context;};// --- Components ---interface DialogProps {children: React.ReactNode;defaultOpen?: boolean;open?: boolean;onOpenChange?: (open: boolean) => void;}const DialogRoot = ({children,defaultOpen = false,open: controlledOpen,onOpenChange,}: DialogProps) => {const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen);const id = React.useId();const isControlled = controlledOpen !== undefined;const open = isControlled ? controlledOpen : uncontrolledOpen;const setOpen = React.useCallback((value: boolean | ((prev: boolean) => boolean)) => {if (!isControlled) {setUncontrolledOpen(value);}if (onOpenChange) {const newValue = typeof value === 'function' ? value(open) : value;onOpenChange(newValue);}},[isControlled, onOpenChange, open],);return <DialogContext value={{ open, setOpen, id }}>{children}</DialogContext>;};interface DialogTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {asChild?: boolean;}const DialogTrigger = ({asChild,onClick,ref,...props}: DialogTriggerProps & { ref?: React.Ref<HTMLButtonElement> }) => {const { setOpen } = useDialogContext();const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {setOpen(true);onClick?.(e);};if (asChild) {return cloneElement(props.children as React.ReactElement<any>, {onClick: handleClick,});}return <button type="button" onClick={handleClick} ref={ref} {...props} />;};DialogTrigger.displayName = 'DialogTrigger';interface DialogPortalProps {children: React.ReactNode;}const DialogPortal = ({ children }: DialogPortalProps) => {const [mounted, setMounted] = React.useState(false);React.useEffect(() => {setMounted(true);}, []);if (!mounted) return null;return createPortal(children, document.body);};interface DialogCloseProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {asChild?: boolean;}const DialogClose = ({asChild,onClick,ref,...props}: DialogCloseProps & { ref?: React.Ref<HTMLButtonElement> }) => {const { setOpen } = useDialogContext();const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {setOpen(false);onClick?.(e);};if (asChild) {return cloneElement(props.children as React.ReactElement<any>, {onClick: handleClick,});}return <button type="button" onClick={handleClick} ref={ref} {...props} />;};DialogClose.displayName = 'DialogClose';type DialogOverlayProps = HTMLMotionProps<'div'> & {className?: string;};const DialogOverlay = ({className,ref,...props}: DialogOverlayProps & { ref?: React.Ref<HTMLDivElement> }) => {const { setOpen } = useDialogContext();return (<motion.divref={ref}variants={OVERLAY_VARIANTS}initial="initial"animate="animate"exit="exit"transition={OVERLAY_TRANSITION}style={OVERLAY_STYLE}className={cn('fixed inset-0 z-200 bg-white/10 dark:bg-black/40', className)}onClick={() => setOpen(false)}{...props}/>);};DialogOverlay.displayName = 'DialogOverlay';type DialogContentProps = {className?: string;children?: React.ReactNode;};const DialogContent = ({className,children,ref,}: DialogContentProps & { ref?: React.Ref<HTMLDivElement> }) => {const { id, open } = useDialogContext();return (<AnimatePresence mode="wait">{open && (<DialogPortal><DialogOverlay /><motion.divref={ref}role="dialog"aria-modal="true"aria-labelledby={`${id}-title`}aria-describedby={`${id}-description`}onClick={(e) => e.stopPropagation()}variants={DIALOG_VARIANTS}initial="initial"animate="animate"exit={{ ...DIALOG_VARIANTS.exit, transition: DIALOG_EXIT_TRANSITION }}transition={DIALOG_TRANSITION}style={DIALOG_STYLE}className={cn('bg-background fixed top-[50%] left-[50%] z-500 grid w-full max-w-xl translate-x-[-50%] translate-y-[-50%] gap-4 rounded-xl border p-6 shadow-lg',className,)}>{children}<DialogClose className="ring-offset-background focus:ring-ring absolute top-4 right-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:pointer-events-none"><HugeiconsIcon icon={Cancel01Icon} className="h-4 w-4" size={16} /><span className="sr-only">Close</span></DialogClose></motion.div></DialogPortal>)}</AnimatePresence>);};DialogContent.displayName = 'DialogContent';interface DialogHeaderProps extends React.HTMLAttributes<HTMLDivElement> {}const DialogHeader = ({ className, ...props }: DialogHeaderProps) => {return (<divclassName={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)}{...props}/>);};DialogHeader.displayName = 'DialogHeader';interface DialogFooterProps extends React.HTMLAttributes<HTMLDivElement> {}const DialogFooter = ({ className, ...props }: DialogFooterProps) => {return (<divclassName={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}{...props}/>);};DialogFooter.displayName = 'DialogFooter';interface DialogTitleProps extends React.HTMLAttributes<HTMLHeadingElement> {}const DialogTitle = ({className,ref,...props}: DialogTitleProps & { ref?: React.Ref<HTMLHeadingElement> }) => {const { id } = useDialogContext();return (<h2id={`${id}-title`}ref={ref}className={cn('text-lg leading-none font-semibold tracking-tight', className)}{...props}/>);};DialogTitle.displayName = 'DialogTitle';interface DialogDescriptionProps extends React.HTMLAttributes<HTMLParagraphElement> {}const DialogDescription = ({className,ref,...props}: DialogDescriptionProps & { ref?: React.Ref<HTMLParagraphElement> }) => {const { id } = useDialogContext();return (<pid={`${id}-description`}ref={ref}className={cn('text-muted-foreground text-sm', className)}{...props}/>);};DialogDescription.displayName = 'DialogDescription';const Dialog = Object.assign(DialogRoot, {Trigger: DialogTrigger,Portal: DialogPortal,Overlay: DialogOverlay,Close: DialogClose,Content: DialogContent,Header: DialogHeader,Footer: DialogFooter,Title: DialogTitle,Description: DialogDescription,});export { Dialog };
Make sure to update the import paths according to your project structure.
Anatomy
import { Dialog } from '@/components/ui/dialog';
<Dialog><Dialog.Trigger asChild><Button>Open Dialog</Button></Dialog.Trigger><Dialog.Content><Dialog.Header><Dialog.Title>Are you absolutely sure?</Dialog.Title><Dialog.Description>This action cannot be undone.</Dialog.Description></Dialog.Header><Dialog.Footer><Dialog.Close asChild><Button variant="outline">Cancel</Button></Dialog.Close><Button>Confirm</Button></Dialog.Footer></Dialog.Content></Dialog>
API Reference
Dialog
The root component that manages dialog state.
| Prop | Type | Default | Description |
|---|---|---|---|
defaultOpen | boolean | false | Initial open state |
open | boolean | - | Controlled open state |
onOpenChange | (open: boolean) => void | - | Callback when open state changes |
DialogTrigger
Button that opens the dialog.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Render as child element |
className | string | - | Additional CSS classes |
DialogContent
The dialog content container with animations.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes |
children | ReactNode | - | Dialog content |
DialogHeader
Container for dialog title and description.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes |
DialogTitle
The dialog title (required for accessibility).
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes |
DialogDescription
Optional description text for the dialog.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes |
DialogFooter
Container for dialog action buttons.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes |
DialogClose
Button that closes the dialog.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Render as child element |
className | string | - | Additional CSS classes |