Skip to content

Dialog

Opens a window over the page for a short task or a message that needs an answer.

Library
What’s this?

Installation

pnpm dlx shadcn@latest add @wisty/dialog

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
        └── DialogClose
import { 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

SLDS mapping

  • DialogOverlay is slds-backdrop.
  • DialogContent is the modal's box, slds-modal__container. Its close button is slds-modal__close.
  • DialogHeader is slds-modal__header, DialogTitle is slds-modal__title, and DialogDescription is 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.
  • DialogFooter is slds-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-300
    • slds-modal_large: md:w-[90%] md:min-w-160 sm:max-w-none
  • The body doesn't scroll on its own, as slds-modal__content does. Wrap it in a div with -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.
PropValueSLDSNotes
DialogContent showCloseButtontrueslds-modal__closeA bordered icon button above the modal's top right corner.
DialogFooter showCloseButtontrueslds-button slds-button_neutralA Close button in the footer.

Accessibility

  • Always include a DialogTitle: it names the dialog. With no DialogDescription, pass aria-describedby={undefined} to DialogContent so the console doesn't warn.
  • A scrolling body needs a way in from the keyboard. Give its div role="region", an aria-label and tabIndex={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 md breakpoint 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 a DialogClose.
  • 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 overflow classes on DialogContent. The close button sits outside the box and would be clipped.