Quire UI
Overlays6

Dialog

Modal dialogs with sticky header and footer, an unsaved-changes guard, stacking and keyboard submit.

Change the name and press Esc: the dialog asks before throwing your edit away. Press Cmd/Ctrl+Enter from the field to save.

Installation

npx shadcn@latest add @quireui/dialog

Usage

<Dialog open={open} onOpenChange={setOpen} dirty={name !== original}>
  <DialogTrigger render={<Button />}>Rename project</DialogTrigger>
  <DialogContent size="sm">
    <DialogForm onFormSubmit={save}>
      <DialogHeader>
        <DialogTitle>Rename project</DialogTitle>
        <DialogDescription>Only visible to people in this workspace.</DialogDescription>
      </DialogHeader>
      <DialogBody>{/* fields */}</DialogBody>
      <DialogFooter>
        <DialogClose render={<Button variant="ghost" />}>Cancel</DialogClose>
        <Button type="submit" variant="primary">Save</Button>
      </DialogFooter>
    </DialogForm>
  </DialogContent>
</Dialog>

Sizes

size on DialogContent: sm, md (default), lg or full. For panels from the edge of the screen, use Sheet.

Only DialogBody scrolls, so the title and the buttons stay in view on long content. It is a Scroll area: a slim scrollbar while scrolling and soft fades where there is more to read.

Unsaved changes

Pass dirty while there are unsaved edits. Esc, an outside click or the close button then show "Discard unsaved changes?" at the bottom of the dialog instead of closing. Focus goes to "Keep editing", and Esc again keeps editing. Set dirty back to false after saving. Change the wording with dirtyMessage.

Accessibility

It is a modal dialog, named by DialogTitle and described by DialogDescription, and the page behind it is hidden from screen readers while it is open. The close button is named "Close", and the unsaved-changes bar is an alert, so it is read out when it appears.

  • The first field is focused when the dialog opens.
  • Cmd/Ctrl+Enter submits the dialog's form from any field, including a textarea where Enter adds a new line.
  • DialogForm validates its fields with the same messages as Input.

Stacking

A dialog opened from inside another one stacks on top, and the one behind steps back so the depth is clear.

For a destructive confirm, use Alert dialog.

API reference

useCloseGuard

While dirty, closing (Esc, outside click, close button) is held back and the CloseGuardBar asks first. "Discard" closes through actionsRef, which the guard lets through.

Prop

Type

CloseGuardBar

The "Discard unsaved changes?" bar. Rendered by DialogContent and SheetContent.

Dialog

Prop

Type

Also takes every prop of Base UI's Dialog.Root.

DialogTrigger

Also takes every prop of Base UI's Dialog.Trigger.

DialogClose

Also takes every prop of Base UI's Dialog.Close.

DialogContent

Prop

Type

Also takes every prop of Base UI's Dialog.Popup. Style it with [data-slot="dialog"].

DialogForm

Wrap header, body and footer to submit with the footer button or Cmd/Ctrl+Enter.

DialogHeader

Also takes every <div> attribute.

DialogBody

The only part that scrolls, so the title and the buttons stay in view.

DialogFooter

Also takes every <div> attribute.

DialogTitle

Also takes every prop of Base UI's Dialog.Title.

DialogDescription

Also takes every prop of Base UI's Dialog.Description.

On this page