Dialog
A dialog is a window that opens over the page and asks for focus. Use it for a task that is better done in one place, away from everything else.
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Field, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
export default function DialogDemo() {
return (
<Dialog>
<DialogTrigger render={<Button />}>Edit profile</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit profile</DialogTitle>
<DialogDescription>Change how you appear on the site. Save when you are done.</DialogDescription>
</DialogHeader>
<Field name="name">
<FieldLabel>Name</FieldLabel>
<Input defaultValue="Njoh Simplice" />
</Field>
<DialogFooter>
<DialogClose render={<Button variant="secondary" />}>Cancel</DialogClose>
<DialogClose render={<Button />}>Save changes</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
}The code
The component is a single file, read from the repository when the site is built. Press Show code to see all of it.
import type { ComponentProps } from "react";
import { X } from "lucide-react";
import { Dialog as BaseDialog } from "@base-ui/react/dialog";
import { Button } from "@/components/ui/button";
import { cx } from "@/lib/cx";
import { backdropStyles, popupMotion, popupSurface } from "@/lib/styles";
import type { WithClass } from "@/lib/types";
export const Dialog = BaseDialog.Root;
// Renders a button by default. To open it from your own element, such as a Button, pass it as
// render: <DialogTrigger render={<Button />}>.
export const DialogTrigger = BaseDialog.Trigger;
// A dim backdrop, and a panel centered on the screen with a close button in the corner. Focus is
// held inside while it is open, and goes back to the trigger when it closes.
export function DialogContent({ className, children, ...props }: WithClass<ComponentProps<typeof BaseDialog.Popup>>) {
return (
<BaseDialog.Portal>
<BaseDialog.Backdrop className={backdropStyles} />
<BaseDialog.Popup
className={cx(
popupSurface,
popupMotion,
"fixed top-1/2 left-1/2 z-50 flex w-[28rem] max-w-[calc(100vw-2rem)] -translate-x-1/2 -translate-y-1/2 flex-col gap-4 p-6",
className,
)}
{...props}
>
{children}
<BaseDialog.Close render={<Button variant="secondary" size="icon" aria-label="Close" className="absolute top-3 right-3" />}>
<X />
</BaseDialog.Close>
</BaseDialog.Popup>
</BaseDialog.Portal>
);
}
export function DialogHeader({ className, ...props }: ComponentProps<"div">) {
return <div className={cx("flex flex-col gap-1.5 pr-10", className)} {...props} />;
}
export function DialogTitle({ className, ...props }: WithClass<ComponentProps<typeof BaseDialog.Title>>) {
return <BaseDialog.Title className={cx("text-xl leading-tight font-semibold text-text", className)} {...props} />;
}
export function DialogDescription({ className, ...props }: WithClass<ComponentProps<typeof BaseDialog.Description>>) {
return <BaseDialog.Description className={cx("text-base text-text/70", className)} {...props} />;
}
export function DialogFooter({ className, ...props }: ComponentProps<"div">) {
return <div className={cx("flex flex-col gap-2 sm:flex-row sm:justify-end", className)} {...props} />;
}
export const DialogClose = BaseDialog.Close;Props
| Prop | Type | Default | What it does |
|---|---|---|---|
defaultOpen | boolean | false | Starts the dialog open, when it manages its own state. |
open | boolean | — | Whether it is open, when you control it yourself. |
onOpenChange | function | — | Called with the new open state. |
render | element | — | On DialogTrigger and DialogClose. Uses your own element, such as a Button, instead of a plain button. |
Every other prop goes to the Base UI Dialog underneath. Always give the dialog a DialogTitle.
The thinking
A dialog interrupts, so it has to be worth it. It is for a self-contained job: edit a profile, confirm a choice, read something before going on.
- Built on Base UI: Base UI holds focus inside, restores it when the dialog closes, locks the page behind and closes on Escape.
- Parts: DialogContent is the window with its backdrop and close button. DialogHeader, DialogTitle, DialogDescription and DialogFooter lay out the inside.
- Dialog or Alert Dialog: A dialog can be dismissed by clicking outside or with the corner button. If the question has to be answered, use an Alert Dialog.
Interface design
- Backdrop: Black at 60%, so the page is still there but clearly out of reach.
- Window: The shared overlay surface, centered, 28rem wide and never wider than the screen minus a 16px margin on each side.
- Footer: Actions sit on the right on desktop. On a phone they stack, with the main action at the bottom under the thumb.
- Close button: An icon-only button in the top corner, for people who want out.
Interaction
- Open: Click the trigger, or press Enter or Space when it has focus.
- Focus: Focus moves into the dialog and Tab stays inside it. When the dialog closes, focus goes back to the trigger.
- Close: Escape, the corner button, a click on the backdrop, or a DialogClose button.
- Scroll: The page behind does not scroll while the dialog is open.
- Motion: A 150ms fade, with a small scale on the window. With reduced motion turned on it appears at once.
Empathy
- Named: The dialog is announced with its title and described by its description.
- Nothing behind is reachable: Screen reader and keyboard users can't wander into the page underneath.
- Always a way out: Escape and the close button are both there, and Cancel sits next to the main action.
- Keep it short: On a small screen a long dialog scrolls badly. If it needs a whole page, make it a page.