Skip to content
EspañolEnglish
All components (204)

Dialog

Layers · Dialog · .amset-dialog

What it is and when to use it

A modal dialog: it blocks progress until it is answered. It is a native <dialog> opened with showModal(): it moves focus inside on opening, keeps it there while open, makes the rest of the page inert and returns focus to the trigger on closing.

When to use it

  • A decision that cannot be ignored, or a short form that makes sense without leaving the screen.
  • kind="alert" for destructive confirmations: it is announced as alertdialog and does not close when clicking outside, so a stray click does not answer on anyone's behalf. Escape does close it, as a cancellation (the WAI-ARIA alertdialog pattern).
  • With trigger it opens and closes on its own; without it, whoever uses it drives it with open and onOpenChange.
  • actions receives close, so the action button closes it without keeping the state outside; footerStart puts something on the left of the footer.
  • loading while its content is arriving: the body has aria-busy and says loadingText ("Loading…").
  • dismissable={false} for a half-filled form that must not be lost by a click outside.

When not to

  • An "are you sure?" attached to the button: ConfirmAction. If the question is asked from code, useConfirmation.
  • Filters or the detail of a row, which slide in from a side: Drawer.
  • A panel attached to its button, without a backdrop: Dropdown.
  • If it needs a lot of scrolling, it is probably a page.

Keyboard: on opening, focus enters; Tab and Shift+Tab stay inside. Escape closes and focus returns to the trigger; in kind="alert" it counts as cancel and reaches onOpenChange(false). Clicking outside closes only the regular one.

Accessibility: it is announced as dialog (or alertdialog) with the title as its name, and description as its accessible description. The X appears with withCloseButton, never on alert dialogs, and is named with closeLabel ("Close the dialog"). The icon of the tone is decoration: the title has to say everything. onClosed arrives when the dialog has already gone (after the exit animation) and focus has already returned: it is the place to move focus elsewhere or to announce something.

Fine-tuning: CSS variables --amset-dialog-*: -width, -padding, -radius, -background and -slot; the backdrop, --amset-overlay-background and --amset-overlay-padding. What is stacked comes from the --amset-layer-* scale (modal is 100).

Custom header: icon (with the color of accent) in place of the tone's, headerActions between the title and the X, and title can be a node. width gives a custom width (a number is pixels, a string is a CSS measure) and takes precedence over size; placement="top" pins it to the top, for what grows downward like a palette; hideHeader hides the header without losing the accessible name; flush removes the body padding and bodyColumn makes it a flex column, for content with its own scrolling.

Embedded: with inline the same box is rendered IN the page, as a region with its title: no <dialog>, no backdrop, no Escape and no focus trap, and no X. It is useful for reusing as a screen panel what is a dialog on another. open shows or hides it.

What scrolls is the body: the header and the footer stay still, and the lines that separate them appear only if the body does not fit entirely. With the whole dialog scrolling, in a long one the title went off the screen (measured: −563 px after scrolling 600) and the buttons only appeared on reaching the end.

What it looks like

<Dialog
  title="Delete the order"
  description="This cannot be undone."
  trigger={<Button variant="soft" tone="error">Delete</Button>}
  actions={(cerrar) => <Button variant="solid" tone="error" onClick={cerrar}>Yes, delete</Button>}
>
  <Text>The shipping history will be lost.</Text>
</Dialog>

Props

Import it with import { Dialog } from '@amset/ui'. Required props are marked.

PropTypeWhat it does
titlerequiredReactNodeThe title. Text, or a node if it carries something more (a mark, a count): the accessible name of the dialog comes from its text content.
accentstringAn own CSS colour for the header (the icon) when neither a tone nor the brand accent will do: a section with its own colour. It is written in `--amset-dialog-accent`.
actions(close: () => void) => ReactNodeFooter buttons. It receives `close` so the action can close the dialog without whoever uses it having to keep the state.
bodyColumnbooleanThe body as a flex column: the content shares out the height and carries its own scroll area, instead of the whole body scrolling.
childrenReactNodeBody of the dialog. Short text: if it needs scrolling, it is probably a page. Without a body (a question that fits in the title and the description) the gap is not painted.
classNamestringMerged with the library classes; it does not replace them.
closeLabelstring—
descriptionReactNodeSentence under the title, before the body. It is used as the accessible description. It can carry more than text (a status label): its text is read.
dismissablebooleanPressing outside closes. By default yes, except in `kind="alert"`, which is never closed that way. With `false`, the dialog only goes away through its buttons, Escape or `open`: for a half-filled form that must not be lost by one click too many (antd's `maskClosable`).
flushbooleanThe body without padding: content that already brings its own, or that reaches the edges.
footerStartReactNodeExtra content on the left of the footer: a help link, a checkbox.
fullbooleanTakes up the whole window minus a margin: for an editor or a preview.
headerActionsReactNodeButtons in the header, between the title and the close cross: a help, a menu.
hideHeaderbooleanNo visible header: the title is still the accessible name, but it is not painted. For what carries its own heading inside, such as a search box.
iconReactNodeOwn icon for the header, instead of the tone's. It inherits the colour of `accent` if given, and otherwise the text colour.
inlinebooleanThe same box painted IN THE PAGE: no `<dialog>`, no backdrop, no Escape or focus trap and no cross. To reuse as a screen panel what elsewhere is a dialog. `open`, if given, shows or hides it.
keyboardDismissDisabledbooleanEscape closes. By default yes. If both are turned off, what is INSIDE has to offer a way out, or whoever does not use a mouse is locked in.
kind"alert" | "normal"`alert` for destructive confirmations: it is announced as `alertdialog` and does not close when clicking outside, so that a stray click does not answer on anyone's behalf. Escape does close it, as a cancellation (the WAI-ARIA `alertdialog` pattern): it reaches `onOpenChange(false)`, just like the cancel button.
loadingbooleanThe content is arriving. A skeleton is shown instead of `children`, and the body is marked busy. The title and the buttons stay: whoever opened it knows what it is about and can close it without waiting.
loadingTextstringWhat is announced to the reader while loading. "Loading…" by default.
onClosed() => voidRuns when it has finished closing: after the exit animation, with the dialog already out of the page and focus returned. Also when whoever controls it closes it with `open={false}`.
onOpenChange(open: boolean) => void—
openboolean—
placement"center" | "top"Where it is placed: in the centre, or stuck to the top (the command palette, which grows downward with its results without moving).
size"sm" | "md" | "lg"Maximum width: 24, 32 or 44 rem.
tone"info" | "success" | "warning" | "error"Color and icon of the header, for confirmations and warnings. Without a tone, the header has no icon.
triggerReactElement<unknown, string | JSXElementConstructor<any>>Element that opens it. If not passed, the dialog is controlled with `open`.
widthstring | numberAn own maximum width, beyond `size`: a number is pixels and a string is a CSS measure (`'60rem'`). It wins over `size`.
withCloseButtonbooleanCross at the top right. It is not shown in an alert dialog: there the decision is taken with the buttons, not by escaping through the corner.

It also accepts the attributes of the element it renders: className, style, data-*, aria-* and its events.

CSS variables

These CSS variables are how you adapt it. Declare them on the component's selector, not on :root: the stylesheet redeclares them in the rule itself, and a declaration on the same element always wins.

VariableDefault
--amset-dialog-width32rem
--amset-dialog-paddingvar(--amset-space-5)
--amset-dialog-radiusvar(--amset-radius-lg)
--amset-dialog-backgroundvar(--amset-surface)
--amset-dialog-slotvar(--amset-space-3)
--amset-dialog-accentcurrentColor

How to change it

.amset-dialog {
  --amset-dialog-width: ...;
  --amset-dialog-padding: ...;
}

In the test bench

Every state and variant, to try one at a time, on its page at ui.amset.tech: