Dialog
Opens a window over the page for a short task or a message that needs an answer.
Installation
The CLI installs the Radix or Base UI file to match your project, with what it builds on: button.
If your project doesn't have Wisty UI's theme yet, add it once with npx shadcn@latest add @wisty/cosmos. Until then, components keep shadcn's default look. Installation has the details.
When to use
Use it
- A short task that needs people's full attention before they go back to the page, like editing a few fields of a record.
- A message people must read and close, like a notice about their trial.
- A choice that stops the current flow until it's made.
Don't use it
- People confirm a destructive action, like deleting a record. Use Alert dialog.
- The content is long or the task has many steps. Use a full page.
- People should keep the page in view while they work. Use Popover for a field or two, or Sheet for a panel beside the page.
- The message needs no answer, like a saved record. Use Sonner.
Usage
Dialog
├── DialogTrigger
└── DialogContent
├── DialogHeader
│ ├── DialogTitle
│ └── DialogDescription
└── DialogFooter
└── DialogCloseimport { buttonVariants } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"<Dialog>
<DialogTrigger className={buttonVariants({ variant: "outline" })}>
Show details
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Acme</DialogTitle>
<DialogDescription>Customer since 2019.</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>DialogContent has a close button just above its top end corner; showCloseButton={false} removes it. showCloseButton on DialogFooter adds a Close button to the footer, as the Message example shows.
To close the dialog when a save finishes, control it. Until setOpen(false) runs, the dialog stays open, so an error can show inside it.
import { useState } from "react"
import { Button, buttonVariants } from "@/components/ui/button"
import {
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogTrigger,
} from "@/components/ui/dialog"
export function EditAccount({ onSave }: { onSave: () => Promise<void> }) {
const [open, setOpen] = useState(false)
async function save() {
await onSave()
setOpen(false)
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger className={buttonVariants({ variant: "outline" })}>
Edit
</DialogTrigger>
<DialogContent>
{/* header and fields */}
<DialogFooter>
<DialogClose className={buttonVariants({ variant: "outline" })}>
Cancel
</DialogClose>
<Button onClick={save}>Save</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}Examples
Message
A short message with a title and text ending in a link, and a Close button added by `showCloseButton` on `DialogFooter`.
Long content
A wider modal, sized like SLDS's medium modal with `className` on `DialogContent`, whose body scrolls while the header and footer stay in view.
API reference
| Part | Built on |
|---|---|
Dialog | Radix Dialog.Root (opens in a new tab) |
DialogTrigger | Radix Dialog.Trigger (opens in a new tab) |
DialogPortal | Radix Dialog.Portal (opens in a new tab) |
DialogClose | Radix Dialog.Close (opens in a new tab) |
DialogOverlay | Radix Dialog.Overlay (opens in a new tab) |
DialogContent | Radix Dialog.Content (opens in a new tab) |
DialogHeader | <div> (opens in a new tab) |
DialogFooter | <div> (opens in a new tab) |
DialogTitle | Radix Dialog.Title (opens in a new tab) |
DialogDescription | Radix Dialog.Description (opens in a new tab) |
DialogContent
| Prop | Type | Default |
|---|---|---|
showCloseButton | boolean | true |
DialogFooter
| Prop | Type | Default |
|---|---|---|
showCloseButton | boolean | false |
SLDS mapping
DialogOverlayisslds-backdrop.DialogContentis the modal's box,slds-modal__container. Its close button isslds-modal__close.DialogHeaderisslds-modal__header,DialogTitleisslds-modal__title, andDialogDescriptionis the header's tagline. A link in it takes the link color, underlined so it doesn't rely on color alone.- Anything between the header and the footer is
slds-modal__content. DialogFooterisslds-modal__footer.
Known gaps:
- SLDS's sizes aren't props. Pass a width through
className, as Long content does:slds-modal_small:md:w-3/5 md:min-w-160 sm:max-w-[52rem]slds-modal_medium:md:w-[70%] md:min-w-160 sm:max-w-300slds-modal_large:md:w-[90%] md:min-w-160 sm:max-w-none
- The body doesn't scroll on its own, as
slds-modal__contentdoes. Wrap it in adivwith-mx-4 -my-1 max-h-[60vh] overflow-y-auto px-4 py-1, as Long content does, so the header and footer stay in view and focus rings aren't clipped. - Icons are lucide's, not SLDS's utility icons.
| Prop | Value | SLDS | Notes |
|---|---|---|---|
DialogContent showCloseButton | true | slds-modal__close | A bordered icon button above the modal's top right corner. |
DialogFooter showCloseButton | true | slds-button slds-button_neutral | A Close button in the footer. |
Accessibility
- Always include a
DialogTitle: it names the dialog. With noDialogDescription, passaria-describedby={undefined}toDialogContentso the console doesn't warn. - A scrolling body needs a way in from the keyboard. Give its
divrole="region", anaria-labelandtabIndex={0}, as Long content does. - Opening the dialog focuses its first element you can tab to, but the bases differ on links. In Radix projects a link is never focused first, so a link in the description is passed over for the next control; in Base UI projects the link takes focus.
- With a Combobox list open inside, the first Escape closes only the list. A Command's search field isn't an open list, so Escape there closes the dialog.
Motion
- The backdrop and the box fade in, in every theme. They only change opacity, so they still run with reduced motion.
Responsive behavior
- On small screens the dialog fills the width less a margin. From the
mdbreakpoint it's half the screen, within a minimum and a maximum width. - The dialog grows with its content and doesn't scroll, so a tall body runs off the screen. Wrap the body in a scrolling
div, as Long content does. - Long words in the title break, and footer buttons wrap onto a second line when they don't fit.
Rules
- Put Cancel before the primary action in
DialogFooter, and make Cancel aDialogClose. - Style triggers and closes with
className={buttonVariants({ variant: "outline" })}. - Don't restyle the dialog's corners, colors or shadow. They come from the theme tokens.
- Don't put
overflowclasses onDialogContent. The close button sits outside the box and would be clipped.
Related
- Alert dialogAsks a question people must answer before going on, like confirming a delete.
- SheetSlides a panel in from the edge of the screen for a task beside the page.
- PopoverShows details or a short form in a panel anchored to a trigger.
- SonnerShows brief, non-blocking messages, like a saved record or a failed request.