Dialog

A window over the page that holds focus until it closes: a form, a confirmation, details.

Installation

npx @yolkui/cli add dialog

Usage

Pass the button that opens it through render: render={<Button variant="outline" />}. DialogContent draws the backdrop, the dialog and an × in the corner; DialogHeader holds the title and description, DialogFooter the actions, at the end, the secondary first and the primary last. Close a dialog with a DialogClose rendered as a Button. Give it an async onClick for a Save: the dialog waits, the button loading, and closes when the work is done (it stays open if the promise rejects or resolves to false; Cancel and the × still leave). Escape, a click on the backdrop and the × close it too, and focus goes back to the button that opened it.

  • showCloseButton={false} leaves the × out. Keep it unless a button in the footer already closes the dialog: on a phone it is the obvious way out.
  • On a phone the dialog sits 16px from the edges and the footer's buttons stack full width, the primary on top.
  • Select, menus and tooltips inside a dialog open above it, and the first Escape closes only them.

Long content

Put long content in a body that scrolls between the header and the footer, so the title and the actions never leave the screen. The body is a plain div with overflow-y-auto (no part needed); -mx-6 px-6 puts its scrollbar at the dialog's edge, and tabIndex={0} with a name lets a keyboard scroll it. Without one the whole dialog scrolls inside, never the page behind it.

Scrolling the whole dialog

scroll="outside" on DialogContent lets a dialog much taller than the screen run past the bottom, and the whole of it scrolls over the page, as a page of its own. For long reading. A form with actions is better with the default, where the buttons stay on screen.

A part outside the dialog

With scroll="outside" the dialog no longer clips at its edge, so a part can sit outside it: a close button above the corner of a preview. Keep it inside DialogContent, so Tab still reaches it and a click on it is not a click outside.

Without the ×

When there is nothing to save and one way out, showCloseButton={false} on DialogContent leaves the × out and showCloseButton on DialogFooter adds an outline Close button (closeLabel translates it). A button of your own, as a DialogClose, works the same.

One dialog, many triggers

For an Edit button on every row, render one dialog and link the buttons to it with a handle: createDialogHandle<Member>(), passed as handle to Dialog and to each DialogTrigger, with the row's data as payload. Dialog's children become a function of the payload. Triggers can sit anywhere on the page, and focus returns to the one that was pressed. handle.open(), openWithPayload() and close() open and close it from code.

  • Ada Lovelaceada@yolkui.com
  • Grace Hoppergrace@yolkui.com
  • Alan Turingalan@yolkui.com

Unsaved changes

To ask before closing a form with changes, control open and cancel the close in onOpenChange (details.cancel()), then open an Alert Dialog. Put it inside DialogContent: it is then nested, the dialog steps back behind it (a little smaller, lower and dimmer) and only one backdrop covers the page. "Keep editing" returns to the form as it was.

API

Our props on top of Base UI's Dialog (open, defaultOpen, onOpenChange, modal, disablePointerDismissal, actionsRef, handle and the rest work as in Base UI). The width is 448; change it with className.

PropOnValuesDefault
scrollDialogContent"inside" · "outside""inside"
showCloseButtonDialogContenttrue · falsetrue, the × in the corner
closeLabelDialogContenttext"Close", the ×'s name
showCloseButtonDialogFootertrue · falsefalse, an outline Close at the end
closeLabelDialogFootertext"Close", the button's text
onClickDialogClosea function; when it returns a promise the dialog waits for it, and stays open on a rejection or false—
loadingDialogClosetrue · false: your own work state; the button loads and your code closes the dialog—
createDialogHandle()—links triggers anywhere to one dialog, with a payload—

Accessibility

The popup is a dialog named by its DialogTitle and described by its DialogDescription. Focus moves to the first control inside when it opens (the dialog itself when opened by touch, so no keyboard pops up), Tab and Shift+Tab stay inside, and the page behind is inert and does not scroll.

  • Always give it a DialogTitle; when the title should not show, give it className="sr-only".
  • The ×'s name is "Close"; closeLabel translates it.
  • For a question that needs an answer before anything else happens (deleting, discarding), use Alert Dialog.

Where focus goes

Focus moves to the first control when the dialog opens and back to its trigger when it closes. initialFocus and finalFocus on DialogContent change that: a ref to the element, or false to leave focus where it is. Send it to the field people came for, and after an action that removes the trigger, to where the result shows.

Members

Leaving the page usable

modal={false} on Dialog keeps the page working behind it: no backdrop, no scroll lock, and focus may leave. Place it out of the way with className. modal="trap-focus" keeps focus inside but still leaves the page clickable. Use these for something that stays open while people work; a question or a form wants the default.

Right to left

Under dir="rtl" the × moves to the top left and the actions to the left on their own. The dialog is portaled to the end of the body, so put dir and lang on <html> (or on DialogContent, as this example does inside an LTR page); wrap the app in Base UI's DirectionProvider as for Tabs.