A modal panel over the map and a color.scrim backdrop, such as the help with the keys or a question before clearing every filter, which holds focus until it closes and then gives it back to the button that opened it.
When to use
Use Dialog for a short modal panel that interrupts, such as the help with the keys, which a press outside closes.
| Use instead | When |
|---|---|
| AlertDialog | The person must answer a question before anything else, such as before clearing every filter |
| DetailPanel | The panel shows a vehicle's or a stop's details beside the map, which stays usable |
| Popover | A short explanation or a few settings sit by the button that opens them, and the rest of the screen stays usable |
| Sheet | The panel slides in from the edge of the screen and floats over the map |
Kongens Nytorv
Metro M1, M2 and M3.
Props
| Name | Type | Default | Description |
|---|---|---|---|
title* | ReactNode | — | The dialog's title, in sentence case, such as "Keys". Required: it names the dialog for screen readers. |
description | ReactNode | — | One sentence under the title that says what the dialog is for, read out with its name when it opens. Phrasing content only: it renders in a <p>. |
children | ReactNode | — | The dialog's body, under the description: sentences, a list or controls. |
actions | ReactNode | — | The dialog's actions, such as Buttons, in a row at its end, the main action last. An action that closes the dialog sets open to false. |
trigger | ReactElement<unknown, JSXElementConstructor<any> | string> | — | The element that opens the dialog, such as a Button, rendered in place of the dialog. Focus returns to it when the dialog closes. Leave it out for a dialog opened only through open. |
open | boolean | — | Whether the dialog is open. Set it to control the dialog, with onClose to hear when it asks to close; leave it out for a dialog its trigger opens. |
defaultOpen | boolean | false | Whether the dialog is open at first, when open is left out. |
onOpen | (event: Event, details: Dialog.Root.ChangeEventDetails) => void | — | Called when the dialog asks to open, from its trigger. details is Base UI's, with its reason and cancel(). |
onClose | (event: Event, reason: Dialog.Root.ChangeEventReason, details: Dialog.Root.ChangeEventDetails) => void | — | Called when the dialog asks to close: reason is 'escape-key', 'outside-press', 'close-press' or another of Base UI's reasons, and details.cancel() keeps it open. |
className | string | — | Reaches the root, the popup. className is merged after pn-Dialog. |
style | CSSProperties | — | Reaches the root, the popup. className is merged after pn-Dialog. |
ref | Ref<HTMLDivElement> | — | Reaches the root, the popup. className is merged after pn-Dialog. |
closeLabel | string | The locale's | Overrides "Close", the close button's name. |
slotProps | { trigger?: Props<unknown> | undefined; portal?: DialogPortalProps | undefined; backdrop?: DialogBackdropProps | undefined; title?: DialogTitleProps | undefined; description?: DialogDescriptionProps | undefined; close?: Omit<DialogCloseProps, "disabled"> | undefined; } | — | Props for each part: trigger, portal, backdrop, title, description, close. |
title (required)ReactNode, default —- The dialog's title, in sentence case, such as "Keys". Required: it names the dialog for screen readers.
descriptionReactNode, default —- One sentence under the title that says what the dialog is for, read out with its name when it opens. Phrasing content only: it renders in a
<p>. childrenReactNode, default —- The dialog's body, under the description: sentences, a list or controls.
actionsReactNode, default —- The dialog's actions, such as Buttons, in a row at its end, the main action last. An action that closes the dialog sets
opentofalse. triggerReactElement<unknown, JSXElementConstructor<any> | string>, default —- The element that opens the dialog, such as a Button, rendered in place of the dialog. Focus returns to it when the dialog closes. Leave it out for a dialog opened only through
open. openboolean, default —- Whether the dialog is open. Set it to control the dialog, with
onCloseto hear when it asks to close; leave it out for a dialog its trigger opens. defaultOpenboolean, defaultfalse- Whether the dialog is open at first, when
openis left out. onOpen(event: Event, details: Dialog.Root.ChangeEventDetails) => void, default —- Called when the dialog asks to open, from its trigger.
detailsis Base UI's, with itsreasonandcancel(). onClose(event: Event, reason: Dialog.Root.ChangeEventReason, details: Dialog.Root.ChangeEventDetails) => void, default —- Called when the dialog asks to close:
reasonis'escape-key','outside-press','close-press'or another of Base UI's reasons, anddetails.cancel()keeps it open. classNamestring, default —- Reaches the root, the popup.
classNameis merged afterpn-Dialog. styleCSSProperties, default —- Reaches the root, the popup.
classNameis merged afterpn-Dialog. refRef<HTMLDivElement>, default —- Reaches the root, the popup.
classNameis merged afterpn-Dialog. closeLabelstring, default The locale's- Overrides "Close", the close button's name.
slotProps{ trigger?: Props<unknown> | undefined; portal?: DialogPortalProps | undefined; backdrop?: DialogBackdropProps | undefined; title?: DialogTitleProps | undefined; description?: DialogDescriptionProps | undefined; close?: Omit<DialogCloseProps, "disabled"> | undefined; }, default —- Props for each part:
trigger,portal,backdrop,title,description,close.
Use it
import { Dialog } from '@mylder/perron'Ask your agent
get_reference({ library: "base-ui", component: "Dialog" })Keyboard and ARIA
From the Base UI reference for Dialog and the installed DialogRoot, DialogTrigger, DialogPopup, DialogBackdrop, DialogTitle and DialogClose. "From" says who does it.
| Key | Does | From |
|---|---|---|
Enter, Space on the trigger | Open the dialog, calling onOpen with reason 'trigger-press', and move focus to the first tabbable element in the popup, the close button, unless initialFocus says otherwise | The browser activates the button; Base UI opens and moves focus |
Tab, Shift+Tab | Move focus through the popup's tabbable elements in reading order, the close button, then the content's and the actions', wrapping from the last to the first and back: focus never leaves while open | Base UI |
Escape | Closes the topmost dialog, calling onClose with reason 'escape-key', and returns focus to the trigger | Base UI |
Enter, Space on the close button | Close the dialog, calling onClose with reason 'close-press', and return focus to the trigger | The browser activates the button; Base UI closes |
A press on the backdrop closes the dialog too, with reason 'outside-press'.
| Element | Role | Accessible name from | States announced |
|---|---|---|---|
trigger | button, native, or the caller's element's own | Its own content, from the caller | aria-haspopup="dialog", aria-expanded, from Base UI |
root | dialog, from Base UI | aria-labelledby the title; described by the description through aria-describedby | Modal by Base UI hiding everything outside it with aria-hidden |
title | heading, level 2, from the native <h2> | Its text, the caller's title | none |
close | button, native | The owned "Close", or closeLabel, in the CloseButton's VisuallyHidden span | none: it is never disabled (CR-6) |
backdrop | presentation, from Base UI | none | none |
header, content, actions | none: plain <div>s | none | none |
What axe won't catch, from the contract, each with how this component meets it:
- Everything the eye is told, the ear is told, for every shape each prop accepts: the dialog is announced with its
titleas its name and itsdescription, the close button'saria-hiddenicon has the owned "Close" as its text, and the scrim only dims what thearia-hiddenoutside the popup already removes. - Every landmark and named region has an accessible name: the dialog is named by its own heading, the
title, througharia-labelledby, andtitleis required. - Every default label can be overridden:
closeLabel. - Every shape the type accepts renders, in every variant:
title,description,childrenandactionsrender a string, an element and a list alike; a long word in the title, the description or the content wraps (CR-9). - A hidden copy kept for animation is
aria-hidden, and a collapsed region isinert: none here; the closed dialog renders nothing, and Base UI hides the page behind the open one witharia-hidden. - Native semantics first: Base UI's
dialogrole, a native<h2>for the title, native<button>s for the trigger and the close button, and no ARIA of Perron's own. - Focus moves in reading order and returns on close: the header comes first in the DOM, then the description, the content and the actions, as on screen; opening moves focus into the popup and closing, by any reason, returns it to the trigger, also for a dialog open at first render (CR-5).
- Every target is at least
size.target-minsquare: the close button,size.control(34 px) square. - Loops stop when the system asks for less motion: none here; the dialog has no loop and no transition (CR-10).
Owned strings
| Key | English | Override | Seen or heard where |
|---|---|---|---|
close | "Close" | closeLabel | close, heard as the close button's name from the CloseButton's VisuallyHidden span; never seen |
close is the key packages/perron/src/locale/en.ts already has, "A button that closes a dialog, panel or notice", so Dialog adds no key. Every other word is the caller's, through title, description, children, actions and trigger.
Do and don't
For a caller using the component, with the reason. Each row is a rule: a must row's Don't is never right, a should row's needs a reason, and Checked by says what catches the Don't in a caller's code.
| Rule | Level | Do | Don't | Checked by |
|---|---|---|---|---|
| DD-1 | must | Give every dialog a short title in sentence case, such as "Keys" or "Clear all filters?", which names it for screen readers | Leave the title empty or hide it: the dialog then has no name | review |
| DD-2 | must | Pass the button that opens the dialog as trigger, so it is announced as opening a dialog and gets focus back | Open a dialog from a button placed beside it with open alone, when a trigger would do: the button loses aria-haspopup and aria-expanded | review |
| DD-3 | must | Set slotProps.trigger.nativeButton to false when trigger is not a <button>, as Base UI warns | Pass a link as the trigger for a dialog: a link goes somewhere, and a dialog opens over the screen | review |
| DD-4 | must | Keep the description to one sentence, and put lists, steps and controls in children | Put block content, such as a list, in description: it renders in a <p> | review |
| DD-5 | must | Put the main action last in actions, the one brand Button, and close the dialog from an action by setting open to false | Mark more than one action brand, or put a second close button in the actions: the header already has one, and Esc closes too | review |
| DD-6 | should | Use a dialog for a question that must be answered or help that interrupts, kept short | Use a dialog for a vehicle's or a stop's details: they belong in the detail panel beside the map, which leaves the map usable | review |
| DD-7 | must | Keep a dialog open, such as while it saves, by calling details.cancel() in onClose | Try to disable the close button: it is never disabled, so touch screen readers can always leave | types |
| DD-8 | must | Leave the close button's label to the locale, or override it with closeLabel | Hide the close button with a className: Base UI asks for it in a modal dialog, so touch screen readers can leave | review |
| DD-9 | must | Let the dialog take z-index.dialog and its place in the middle of the screen | Set a z-index, a position or an opacity on the dialog: it would fall under another overlay or leave its scrim | review |