molecule · stable · since 2026-09
Dialog
Purpose
A modal dialog on the native dialog element and nothing else. The element gives, correctly and for free, the four behaviours every hand-made modal gets wrong at least once: the focus is held inside, Escape closes, the page behind is inert, and the focus goes back to the button that opened it. The component adds the name, a labelled close button, a bindable open and the value of the button that closed it.
When to use, when not
Use it for a question the user must answer before going on, a short form, a list to pick from. Do not use it for a message that needs no answer (Alert, or LiveRegion for one that appears after an action), for content that could sit on the page, or for a confirmation of a destructive action, which ConfirmDialog composes from this one.
Anatomy
- The heading, which names the dialog.
- The body.
- The actions, optional: buttons of type submit inside a
form method="dialog", each with a value. - The close button, last in the DOM and drawn top right, labelled “Fermer”.
- The backdrop, the one veil the system allows.
Props
| Name | Type | Default | What it does |
|---|---|---|---|
open? | boolean | undefined | Shown or not; absent reads as closed. Bindable, and set back to false when the dialog closes by any means: Escape, the close button, an action. | |
title | string | The heading, which names the dialog. A question or a noun phrase, French. | |
size? | 'sm' | 'md' | 'lg' | 'sm' | `sm` for a question, `md` for a short form, `lg` for a list or a table. |
onclose? | (returnValue: string) => void | Called after the dialog closed, with the value of the button that closed it, or an empty string for Escape and the close button. | |
closeLabel? | string | 'Fermer' | The close button's label. |
class? | string | '' | Layout classes only: margins, never appearance. |
children | Snippet | The body. | |
actions? | Snippet | The actions at the foot: buttons of type submit in a `form method="dialog"`, whose value comes back through `onclose`. |
States
Closed, then open as a question or as a form; the outcome is read from the value of the button that closed it.
form
A form inside: the first field takes the initial focus, since the close button is last in the DOM.
Light
Dark
320 px, light
320 px, dark
216 px, light
Forced colours, captured
question
Opened by the button; Escape, the close button and the two actions all close it, and the outcome is read from the value of the button that did.
Light
Dark
320 px, light
320 px, dark
216 px, light
Forced colours, captured
Accessibility contract
- Role and name:
dialog, named by the heading througharia-labelledby. The heading is anh2; the page’s own structure stops at the dialog. - Keyboard, focus:
showModalholds Tab inside. The initial focus goes to the first focusable element of the body, or to an element markedautofocus; the close button is last in the DOM for that reason. Escape closes. On close, the browser returns the focus to the opener. - Live regions: none. The outcome, when it must be said, is the page’s, through LiveRegion.
- Forced colours: the surface and the line take the system’s colours; the backdrop takes Canvas.
Content rules
French. The title is a question (“Retirer la page de l’audit ?”) or a noun phrase (“Renommer l’audit”). An action button says what it does with a verb (“Retirer la page”, “Enregistrer”), never “OK” or “Oui”. The cancel button says “Annuler”.
Do and don’t
- Do bind
openand set it from the page; never callshowModalyourself. - Do give every action button a value: it is what
onclosehands back. - Do not put a second dialog inside a dialog.
- Do not open a dialog at page load or after a delay: it takes the focus from wherever the user was.
Examples
<Button onclick={() => (open = true)}>Retirer la page</Button>
<Dialog bind:open title="Retirer la page de l'audit ?" onclose={(value) => value === 'confirm' && remove()}>
<p>Ses vérifications seront conservées, mais la page ne comptera plus.</p>
{#snippet actions()}
<form method="dialog">
<Button variant="secondary" type="submit" value="cancel">Annuler</Button>
<Button type="submit" value="confirm">Retirer la page</Button>
</form>
{/snippet}
</Dialog> Surfaces
- App: the confirmation panel of the audit page and the
confirm()calls, the page switcher, the export dialog; not ported yet. - Extension: the page switcher and the export and sync dialogs of the panel, when the panel is on Svelte.
Related
ConfirmDialog, Button, LiveRegion.
Decisions
- 2026-09-24: the native element, no focus trap of our own. The site’s modale demo proved the four behaviours come from
showModal; a trap written by hand was the extension’s second most patched file. - 2026-09-24: the close button is last in the DOM. A dialog whose first stop is “Fermer” announces its exit before its question.
- 2026-09-24: the outcome travels as the closing button’s value through
onclose, not as one handler per button, so a confirmation is one function and reads the same whether the user pressed Escape, the close button or “Annuler”.
Criteria held by construction
- 7.1, Chaque script est-il, si nécessaire, compatible avec les technologies d'assistance ?
The native element carries the dialog role and the name comes from its heading through aria-labelledby; nothing is declared by hand, so nothing can be declared wrong. - 7.3, Chaque script est-il contrôlable par le clavier et par tout dispositif de pointage ?
showModal holds the focus inside, Escape closes, the close button is a real button: the whole dialog is driven from the keyboard by the browser itself. - 12.8, Dans chaque page web, l'ordre de tabulation est-il cohérent ?
When the dialog closes, the browser puts the focus back on the element that opened it, so the tab order resumes where the user left it. - 12.9, Dans chaque page web, la navigation ne doit pas contenir de piège au clavier. Cette règle est-elle respectée ?
The focus is held inside while the dialog is open, and Escape always releases it: a trap with an exit is not a trap.
Tokens it reads
- Colour:
--rl-backdrop,--rl-ink,--rl-ink-soft,--rl-line-control,--rl-line-strong,--rl-surface - Typography:
--rl-weight-bold,--rl-text-xl,--rl-tracking-tight - Spacing:
--rl-space-12,--rl-space-3,--rl-space-4,--rl-space-5,--rl-space-8,--rl-control-sm - Layout:
--rl-radius,--rl-border,--rl-border-strong
And one custom property of its own, not tokens: --rl-dialog-width.