Dialog
A popup that opens on top of the entire page.
Installation
pnpm dlx shadcn@latest add @lumi-ui/dialog
Basic Usage
import {
Dialog,
DialogClose,
DialogDescription,
DialogFooter,
DialogHeader,
DialogContent,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
export function DialogDemo() {
return (
<Dialog>
<DialogTrigger>Show Alert Dialog</DialogTrigger>
<DialogContent showCloseButton>
<DialogHeader>
<DialogTitle>Are you absolutely sure?</DialogTitle>
<DialogDescription>
This action cannot be undone. This will permanently delete your
account and remove your data from our servers.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose>Cancel</DialogClose>
<DialogClose>Continue</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
}Anatomy
<Dialog>
<DialogTrigger />
<DialogContent>
<DialogHeader>
<DialogTitle />
<DialogDescription />
</DialogHeader>
<DialogScrollArea />
<DialogFooter>
<DialogClose />
</DialogFooter>
</DialogContent>
</Dialog>Cookbook
Controlled dialog
By default, Dialog is an uncontrolled component that manages its own state. Use the open and onOpenChange props to control the dialog state. This is useful when triggering the dialog from external events or performing async actions before closing.
const [open, setOpen] = React.useState(false);
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger>Open</DialogTrigger>
<DialogContent>
<form
// Close the dialog once the form data is submitted
onSubmit={
async () => {
await submitData();
setOpen(false);
}
}
>
...
</form>
</DialogContent>
</Dialog>It’s also common to use onOpenChange if your app needs to do something when the dialog is closed or opened. This is recommended over useEffect when reacting to state changes.
<Dialog
open={open}
onOpenChange={(open) => {
// Do stuff when the dialog is closed
if (!open) {
doStuff();
}
// Set the new state
setOpen(open);
}}
>Open from Dropdown menu
To open a dialog from a DropdownMenu, use the controlled state pattern. The Dialog should be placed outside the DropdownMenu to avoid nesting issues.
Nested dialogs
When opening a dialog on top of another, use the DialogStackedContent component. This component includes specific CSS logic to create a visual "stacking" scale effect, allowing the parent dialog to recede slightly when the child opens.
Close confirmation
This example demonstrates a nested confirmation dialog that triggers if the user attempts to discard unsaved input. To achieve the correct visual stacking effect where the parent form recedes into the background, use DialogStackedContent and AlertDialogStackedContent.
Inside scroll
Use DialogScrollableContent combined with DialogScrollArea. This layout fixes the header and footer in place while allowing the inner content body to scroll independently, ensuring the modal never exceeds the viewport height.
Outside scroll
To allow the entire dialog—including the header and footer—to scroll with the page, use DialogViewport as the scrolling container. This approach is useful for very long content where internal scrolling is not desired.
Placing elements outside the popup
To visually place elements (like a close button) outside the main popup surface while keeping them accessible and within the focus trap, nest them inside DialogPopup. The DialogPopup container has pointer-events: none by default, while the inner content wrapper has pointer-events: auto. You must explicitly apply pointer-events-auto to any detached elements to make them interactive.
Detached Triggers
If you need to trigger a dialog from a completely different part of the component tree, or if nesting the trigger inside the root is impractical, use createDialogHandle to link a DialogTrigger to a Dialog remotely.
import { createDialogHandle } from "@/components/ui/dialog"
const demoDialog = createDialogHandle();
<DialogTrigger handle={demoDialog}>Open</DialogTrigger>
<Dialog handle={demoDialog}>
...
</Dialog>Multiple triggers & payloads
A single dialog can serve multiple triggers. You can pass a payload from the trigger to the dialog to render dynamic content based on which element opened it. This is highly effective for list views where every item needs an "Edit" or "Delete" modal without rendering a separate dialog instance for each row.
<Dialog>
<DialogTrigger>Trigger 1</DialogTrigger>
<DialogTrigger>Trigger 2</DialogTrigger>
...
</Dialog>const demoDialog = createDialogHandle();
<DialogTrigger handle={demoDialog}>Trigger 1</DialogTrigger>
<DialogTrigger handle={demoDialog}>Trigger 2</DialogTrigger>
<Dialog handle={demoDialog}>
...
</Dialog>The dialog can render different content depending on which trigger opened it. This is achieved by passing a payload to the <DialogTrigger> and using the function-as-a-child pattern in <Dialog>.
Controlled mode with multiple triggers
When controlling a multi-trigger dialog externally, you must manage both the open state and the triggerId. The onOpenChange event provides eventDetails containing the ID of the trigger that initiated the action.