Dialog

A modal dialog that interrupts the user with important content and expects a response.

1'use client';
2
3import { Button } from '@/components/ui/button';
4import { Dialog } from '@/components/ui/dialog';
5
6export 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 your
17 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.div
ref={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.div
ref={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 (
<div
className={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 (
<div
className={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 (
<h2
id={`${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 (
<p
id={`${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.
PropTypeDefaultDescription
defaultOpenbooleanfalseInitial open state
openboolean-Controlled open state
onOpenChange(open: boolean) => void-Callback when open state changes

DialogTrigger

Button that opens the dialog.
PropTypeDefaultDescription
asChildbooleanfalseRender as child element
classNamestring-Additional CSS classes

DialogContent

The dialog content container with animations.
PropTypeDefaultDescription
classNamestring-Additional CSS classes
childrenReactNode-Dialog content

DialogHeader

Container for dialog title and description.
PropTypeDefaultDescription
classNamestring-Additional CSS classes

DialogTitle

The dialog title (required for accessibility).
PropTypeDefaultDescription
classNamestring-Additional CSS classes

DialogDescription

Optional description text for the dialog.
PropTypeDefaultDescription
classNamestring-Additional CSS classes

DialogFooter

Container for dialog action buttons.
PropTypeDefaultDescription
classNamestring-Additional CSS classes

DialogClose

Button that closes the dialog.
PropTypeDefaultDescription
asChildbooleanfalseRender as child element
classNamestring-Additional CSS classes