A dialog is a modal window that requires user interaction before returning to the main content. It’s useful for capturing user input, confirming actions, or displaying important information that requires immediate attention.
Installation
npx shadcn@latest add @eo-n/dialog
Install dependencies
npm install @base-ui/react lucide-react
Copy component code
Copy and paste the following code into components/ui/dialog.tsx:"use client";
import * as React from "react";
import { Dialog as DialogPrimitive } from "@base-ui/react";
import { XIcon } from "lucide-react";
import { cn } from "@/lib/utils";
const Dialog = DialogPrimitive.Root;
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
}
function DialogBackdrop({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Backdrop>) {
return (
<DialogPrimitive.Backdrop
data-slot="dialog-backdrop"
className={cn(
"fixed inset-0 z-50 bg-black/50 backdrop-blur-[1.5px] transition-opacity duration-150 ease-out data-[ending-style]:opacity-0 data-[starting-style]:opacity-0",
className
)}
{...props}
/>
);
}
interface DialogContentProps
extends React.ComponentProps<typeof DialogPrimitive.Popup> {
hideCloseIcon?: boolean;
flush?: boolean;
}
function DialogContent({
className,
children,
hideCloseIcon = false,
flush = false,
...props
}: DialogContentProps) {
return (
<DialogPortal>
<DialogBackdrop />
<DialogPrimitive.Viewport className="fixed inset-0 z-50 flex items-center justify-center overflow-hidden p-4">
<DialogPrimitive.Popup
data-slot="dialog-content"
data-flush={flush}
className={cn(
"bg-background group fixed flex max-h-[calc(100%-2rem)] w-full max-w-[calc(100%-2rem)] flex-col gap-4 overflow-hidden rounded-lg border p-6 shadow-lg transition-all duration-150 ease-out outline-none sm:max-w-lg",
flush && "gap-0 p-0",
className
)}
{...props}
>
{children}
{!hideCloseIcon && (
<DialogPrimitive.Close className="absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100">
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Popup>
</DialogPrimitive.Viewport>
</DialogPortal>
);
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn(
"flex flex-col gap-2 text-center sm:text-left",
"group-data-[flush=true]:border-b group-data-[flush=true]:p-6",
className
)}
{...props}
/>
);
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
"group-data-[flush=true]:bg-muted/60 group-data-[flush=true]:border-t group-data-[flush=true]:p-6",
className
)}
{...props}
/>
);
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
);
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogBackdrop,
DialogPortal,
DialogTitle,
DialogTrigger,
};
Update imports
Update the import paths to match your project setup.
Usage
Import all parts and piece them together:
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
<Dialog>
<DialogTrigger>Open</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Are you sure you want to proceed?</DialogTitle>
<DialogDescription>
This action may have permanent effects. Please confirm if you want to
continue.
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
Examples
Basic Dialog
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
export default function DialogDemo() {
return (
<Dialog>
<DialogTrigger asChild>
<Button>Open Dialog</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Are you sure you want to proceed?</DialogTitle>
<DialogDescription>
This action may have permanent effects. Please confirm if you want
to continue.
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
);
}
With Custom Close Button
You can add a custom close button in the footer along with action buttons:
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
export default function DialogWithFooter() {
return (
<Dialog>
<DialogTrigger asChild>
<Button>Open</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Confirm Action</DialogTitle>
<DialogDescription>
Are you sure you want to proceed with this action?
</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button>Confirm</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
Without Close Icon
Use the hideCloseIcon prop to remove the X button from the dialog:
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
export default function DialogNoCloseIcon() {
return (
<Dialog>
<DialogTrigger asChild>
<Button>Open</Button>
</DialogTrigger>
<DialogContent hideCloseIcon>
<DialogHeader>
<DialogTitle>No Close Icon</DialogTitle>
<DialogDescription>
This dialog doesn't have a close icon. Use the escape key or click
outside to close.
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
);
}
Nested Dialogs
Dialogs can be nested to create multi-step flows:
import * as React from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
export default function NestedDialogs() {
return (
<Dialog>
<DialogTrigger asChild>
<Button>Open First Dialog</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>First Dialog</DialogTitle>
<DialogDescription>
This is the first dialog. You can open another dialog from here.
</DialogDescription>
</DialogHeader>
<Dialog>
<DialogTrigger asChild>
<Button>Open Second Dialog</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Second Dialog</DialogTitle>
<DialogDescription>
This is a nested dialog on top of the first one.
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
</DialogContent>
</Dialog>
);
}
Control the dialog state to open it from a dropdown menu:
import * as React from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
export default function DialogFromMenu() {
const menuTriggerRef = React.useRef<HTMLButtonElement>(null);
const [dialogOpen, setDialogOpen] = React.useState(false);
return (
<>
<DropdownMenu>
<DropdownMenuTrigger ref={menuTriggerRef} asChild>
<Button>Open Menu</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={() => setDialogOpen(true)}>
Open Dialog
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent finalFocus={menuTriggerRef}>
<DialogHeader>
<DialogTitle>Dialog from Menu</DialogTitle>
<DialogDescription>
This dialog was opened from a menu item. Focus will return to
the menu trigger when closed.
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
</>
);
}
Make sure to use the dialog’s finalFocus prop to return focus back to the menu trigger for proper accessibility.
API Reference
Dialog
The root component that manages the dialog state.
Controls the open state of the dialog.
The initial open state for uncontrolled usage.
Callback fired when the open state changes.
DialogTrigger
The button that opens the dialog.
Merge props onto the child element instead of wrapping it.
DialogContent
Contains the content to be rendered in the open dialog.
Hides the X close icon button.
Removes padding and gap from the content container. Useful when using DialogHeader and DialogFooter with borders.
finalFocus
React.RefObject<HTMLElement>
Element to receive focus when the dialog closes.
Additional CSS classes to apply.
Wrapper for the dialog title and description.
Additional CSS classes to apply.
Wrapper for dialog action buttons.
Additional CSS classes to apply.
DialogTitle
The accessible title of the dialog.
Additional CSS classes to apply.
DialogDescription
The accessible description of the dialog.
Additional CSS classes to apply.
DialogClose
A button that closes the dialog.
Merge props onto the child element instead of wrapping it.