Overlaysstable.dialog
Dialog
A modal window on native <dialog> for a short, focused task: header, body, footer actions.
Use a dialog for a small task that belongs to the current page and must be finished or cancelled before going on: change a group, add a note, pick a date. Longer or multi-step tasks get their own page; on phones, contextual details are often better in a bottom sheet.
Built on <dialog> + showModal(): top layer, focus trap, inert background and Esc come from the browser. The content is a <form method="dialog">, so every footer button closes the dialog and sets dialog.returnValue to its value. Esc, the close button and a click on the backdrop all close with cancel; focus then returns to the opening button.
This renderer takes text paragraphs and optional form fields as data. Apps compose richer bodies (lists, alerts, belt pickers) by rendering other components into .dialog__body themselves.
For "are you sure" questions about irreversible actions use the confirm dialog, which names the consequences.
Examples
Form dialog with trigger
Data
render({
"id": "change-group",
"title": "Trainingsgruppe ändern",
"trigger": {
"label": "Gruppe ändern …",
"icon": "users"
},
"paragraphs": [
"Lea Müller wechselt ab dem nächsten Training in die neue Gruppe."
],
"fields": [
{
"id": "new-group",
"label": "Neue Gruppe",
"control": "select",
"value": "kids",
"options": [
{
"value": "kids",
"label": "Kinder 7–10"
},
{
"value": "youth",
"label": "Jugend 11–15"
}
]
}
],
"actions": [
{
"label": "Abbrechen",
"value": "cancel"
},
{
"label": "Gruppe ändern",
"value": "save",
"variant": "primary"
}
]
})Markup
<button class="button" type="button" data-behavior="open-dialog" data-target="change-group"><svg class="icon" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#users"/></svg>Gruppe ändern …</button>
<dialog class="dialog" id="change-group" aria-labelledby="change-group-title" aria-describedby="change-group-body">
<form method="dialog" class="dialog__inner">
<header class="dialog__header"><h2 id="change-group-title">Trainingsgruppe ändern</h2><button class="button" data-variant="ghost" data-icon-only value="cancel" formnovalidate aria-label="Schliessen"><svg class="icon" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#x"/></svg></button></header>
<div class="dialog__body">
<div class="stack" data-gap="sm" id="change-group-body"><p>Lea Müller wechselt ab dem nächsten Training in die neue Gruppe.</p></div>
<div class="field">
<label class="field__label" for="new-group">Neue Gruppe</label>
<select class="select" id="new-group" name="new-group"><option value="kids" selected>Kinder 7–10</option><option value="youth">Jugend 11–15</option></select>
</div>
</div>
<footer class="dialog__footer button-row" data-stack-mobile><button class="button" value="cancel" formnovalidate>Abbrechen</button><button class="button" value="save" data-variant="primary">Gruppe ändern</button></footer>
</form>
</dialog>Information, large
Data
render({
"id": "exam-rules",
"title": "Ablauf der Gurtprüfung",
"size": "lg",
"paragraphs": [
"Die Prüfung beginnt um 10:00 im Dojang Bern. Bitte 30 Minuten vorher umgezogen bereit sein.",
"Geprüft werden Form, Kyorugi, Hosinsul und Theorie. Die Resultate erscheinen am selben Abend im Mitgliederbereich."
],
"actions": [
{
"label": "Verstanden",
"value": "close",
"variant": "primary"
}
]
})Markup
<dialog class="dialog" id="exam-rules" data-size="lg" aria-labelledby="exam-rules-title" aria-describedby="exam-rules-body">
<form method="dialog" class="dialog__inner">
<header class="dialog__header"><h2 id="exam-rules-title">Ablauf der Gurtprüfung</h2><button class="button" data-variant="ghost" data-icon-only value="cancel" formnovalidate aria-label="Schliessen"><svg class="icon" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#x"/></svg></button></header>
<div class="dialog__body">
<div class="stack" data-gap="sm" id="exam-rules-body"><p>Die Prüfung beginnt um 10:00 im Dojang Bern. Bitte 30 Minuten vorher umgezogen bereit sein.</p><p>Geprüft werden Form, Kyorugi, Hosinsul und Theorie. Die Resultate erscheinen am selben Abend im Mitgliederbereich.</p></div>
</div>
<footer class="dialog__footer button-row" data-stack-mobile><button class="button" value="close" data-variant="primary">Verstanden</button></footer>
</form>
</dialog>Data contract
What render(data) accepts — validated against this JSON Schema in development and tests. Unknown fields are rejected. The same schema is in components.json.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id | string | required | — | Id of the <dialog> (target of the opening button); prefix for {id}-title and {id}-body. Must be unique on the page.pattern ^[A-Za-z][\w-]*$ |
title | string | required | — | Heading naming the task ("Trainingsgruppe ändern"). min 1 chars |
paragraphs | array of string | optional | — | Body text, one string per paragraph. Linked as the dialog's description. |
fields | array of objects | optional | — | Form fields in the body (see Form field). Their values are in the form when the dialog closes. |
fields[].id | string | required | — | Id of the control; also prefix for {id}-hint and {id}-error. Must be unique on the page.pattern ^[A-Za-z][\w-]*$ |
fields[].label | string | required | — | Visible label ("E-Mail", "Geburtsdatum"). min 1 chars |
fields[].control | "input" | "select" | "textarea" | optional | "input" | Which control to render. |
fields[].type | "text" | "email" | "tel" | "date" | "time" | "number" | "url" | "search" | "password" | optional | "text" | Input type (control = input). |
fields[].name | string | optional | — | Form field name; defaults to id. |
fields[].value | string | number | optional | — | Current value. |
fields[].placeholder | string | optional | — | Example value ("z. B. 3011"). Never a substitute for the label. |
fields[].hint | string | optional | — | Help text shown between label and control. |
fields[].error | string | optional | — | Error message; when set the control is marked invalid. Say what to do: "Bitte gib … ein". |
fields[].required | boolean | optional | false | Field must be filled. Optional fields are marked "(optional)" instead of marking required ones. |
fields[].optional | boolean | optional | false | Append "(optional)" to the label (use in forms where most fields are required). |
fields[].disabled | boolean | optional | false | Read-only for now; explain why in the hint. |
fields[].autocomplete | string | optional | — | HTML autocomplete token, e.g. email, given-name, bday, one-time-code. |
fields[].inputmode | "text" | "numeric" | "decimal" | "tel" | "email" | "url" | "search" | optional | — | Virtual keyboard hint, e.g. numeric for PLZ. |
fields[].maxlength | integer | optional | — | Maximum characters. ≥ 1 |
fields[].rows | integer | optional | 4 | Visible lines (control = textarea). ≥ 2 |
fields[].options | array of objects | optional | — | Choices (control = select). Put a sensible default first or add an empty "Bitte wählen" option. |
fields[].options[].value | string | required | — | Submitted value. |
fields[].options[].label | string | required | — | Shown text. |
fields[].options[].disabled | boolean | optional | — | Not selectable (e.g. full group). |
size | "md" | "lg" | optional | "md" | md 32 rem wide, lg 48 rem for tables or previews. |
closeLabel | string | optional | "Schliessen" | Accessible name of the ✕ button in the header. |
showClose | boolean | optional | true | Show the ✕ button in the header. |
actions | array of objects | required | — | Footer buttons in display order — the primary action last. Each closes the dialog with its value as dialog.returnValue.min 1 items · max 3 items |
actions[].label | string | required | — | Button text, a verb naming the result ("Gruppe ändern"); "Abbrechen" for the cancel button. min 1 chars |
actions[].value | string | required | — | Return value. Use cancel for the dismiss button (it skips form validation).pattern ^[a-z][a-z0-9-]*$ |
actions[].variant | "secondary" | "primary" | "ghost" | "danger" | optional | "secondary" | Button variant; one primary (or danger for destructive) per dialog. |
trigger | object | optional | — | Optional opening button rendered before the dialog (data-behavior="open-dialog"). |
trigger.label | string | required | — | Button text; ends with "…" when it opens a dialog ("Gruppe ändern …"). min 1 chars |
trigger.variant | "secondary" | "primary" | "ghost" | "danger" | "danger-ghost" | optional | "secondary" | Visual weight / meaning. |
trigger.icon | string | optional | — | Icon id shown before the label. |
JSON Schema
{
"type": "object",
"additionalProperties": false,
"required": [
"id",
"title",
"actions"
],
"properties": {
"id": {
"type": "string",
"pattern": "^[A-Za-z][\\w-]*$",
"description": "Id of the `<dialog>` (target of the opening button); prefix for `{id}-title` and `{id}-body`. Must be unique on the page."
},
"title": {
"type": "string",
"minLength": 1,
"description": "Heading naming the task (\"Trainingsgruppe ändern\")."
},
"paragraphs": {
"type": "array",
"description": "Body text, one string per paragraph. Linked as the dialog's description.",
"items": {
"type": "string",
"minLength": 1,
"description": "One paragraph."
}
},
"fields": {
"type": "array",
"description": "Form fields in the body (see Form field). Their values are in the form when the dialog closes.",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"id",
"label"
],
"properties": {
"id": {
"type": "string",
"pattern": "^[A-Za-z][\\w-]*$",
"description": "Id of the control; also prefix for `{id}-hint` and `{id}-error`. Must be unique on the page."
},
"label": {
"type": "string",
"minLength": 1,
"description": "Visible label (\"E-Mail\", \"Geburtsdatum\")."
},
"control": {
"type": "string",
"enum": [
"input",
"select",
"textarea"
],
"default": "input",
"description": "Which control to render."
},
"type": {
"type": "string",
"enum": [
"text",
"email",
"tel",
"date",
"time",
"number",
"url",
"search",
"password"
],
"default": "text",
"description": "Input type (control = input)."
},
"name": {
"type": "string",
"description": "Form field name; defaults to `id`."
},
"value": {
"type": [
"string",
"number"
],
"description": "Current value."
},
"placeholder": {
"type": "string",
"description": "Example value (\"z. B. 3011\"). Never a substitute for the label."
},
"hint": {
"type": "string",
"description": "Help text shown between label and control."
},
"error": {
"type": "string",
"description": "Error message; when set the control is marked invalid. Say what to do: \"Bitte gib … ein\"."
},
"required": {
"type": "boolean",
"default": false,
"description": "Field must be filled. Optional fields are marked \"(optional)\" instead of marking required ones."
},
"optional": {
"type": "boolean",
"default": false,
"description": "Append \"(optional)\" to the label (use in forms where most fields are required)."
},
"disabled": {
"type": "boolean",
"default": false,
"description": "Read-only for now; explain why in the hint."
},
"autocomplete": {
"type": "string",
"description": "HTML autocomplete token, e.g. `email`, `given-name`, `bday`, `one-time-code`."
},
"inputmode": {
"type": "string",
"enum": [
"text",
"numeric",
"decimal",
"tel",
"email",
"url",
"search"
],
"description": "Virtual keyboard hint, e.g. `numeric` for PLZ."
},
"maxlength": {
"type": "integer",
"minimum": 1,
"description": "Maximum characters."
},
"rows": {
"type": "integer",
"minimum": 2,
"default": 4,
"description": "Visible lines (control = textarea)."
},
"options": {
"type": "array",
"description": "Choices (control = select). Put a sensible default first or add an empty \"Bitte wählen\" option.",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"value",
"label"
],
"properties": {
"value": {
"type": "string",
"description": "Submitted value."
},
"label": {
"type": "string",
"description": "Shown text."
},
"disabled": {
"type": "boolean",
"description": "Not selectable (e.g. full group)."
}
}
}
}
},
"description": "One form field."
}
},
"size": {
"type": "string",
"enum": [
"md",
"lg"
],
"default": "md",
"description": "`md` 32 rem wide, `lg` 48 rem for tables or previews."
},
"closeLabel": {
"type": "string",
"default": "Schliessen",
"description": "Accessible name of the ✕ button in the header."
},
"showClose": {
"type": "boolean",
"default": true,
"description": "Show the ✕ button in the header."
},
"actions": {
"type": "array",
"minItems": 1,
"maxItems": 3,
"description": "Footer buttons in display order — the primary action last. Each closes the dialog with its `value` as `dialog.returnValue`.",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"label",
"value"
],
"properties": {
"label": {
"type": "string",
"minLength": 1,
"description": "Button text, a verb naming the result (\"Gruppe ändern\"); \"Abbrechen\" for the cancel button."
},
"value": {
"type": "string",
"pattern": "^[a-z][a-z0-9-]*$",
"description": "Return value. Use `cancel` for the dismiss button (it skips form validation)."
},
"variant": {
"type": "string",
"enum": [
"secondary",
"primary",
"ghost",
"danger"
],
"default": "secondary",
"description": "Button variant; one `primary` (or `danger` for destructive) per dialog."
}
}
}
},
"trigger": {
"type": "object",
"additionalProperties": false,
"required": [
"label"
],
"description": "Optional opening button rendered before the dialog (`data-behavior=\"open-dialog\"`).",
"properties": {
"label": {
"type": "string",
"minLength": 1,
"description": "Button text; ends with \"…\" when it opens a dialog (\"Gruppe ändern …\")."
},
"variant": {
"type": "string",
"enum": [
"secondary",
"primary",
"ghost",
"danger",
"danger-ghost"
],
"default": "secondary",
"description": "Visual weight / meaning."
},
"icon": {
"type": "string",
"description": "Icon id shown before the label."
}
}
}
}
}Markup & states
Root: dialog.dialog — usable without render() by writing the markup directly.
Attributes
| Attribute | Values | Default | Description |
|---|---|---|---|
aria-labelledby | "{id}-title" | — | Named by its heading. |
aria-describedby | "{id}-body" | — | Body text as description (only when there are paragraphs). |
data-size | lg | (none = md) | Width. |
Parts
| Part | Description |
|---|---|
.dialog__inner | <form method="dialog"> wrapping everything. |
.dialog__header | Title (h2) and ✕ button (value="cancel"). |
.dialog__body | Scrollable content. |
.dialog__footer.button-row | Actions; stacked full width on phones with the primary at the bottom (data-stack-mobile). |
States
| State | Description |
|---|---|
[open] | Shown (with entry animation). |
::backdrop | Dimmed page behind; a click on it closes with cancel. |
Behaviour & events
Module: js/dialog.js
Declarative: a button with data-behavior="open-dialog" data-target="{id}" and mount(root). Imperative: openDialog(dialogEl, openerEl).
API
| Function | Signature | Returns | Description |
|---|---|---|---|
openDialog | openDialog(dialog: HTMLDialogElement, opener?: HTMLElement) | void | Opens with showModal(), closes on backdrop click (returnValue = "cancel"), returns focus to opener (default: the active element) on close. |
Events
| Event | Detail | Description |
|---|---|---|
close (on the dialog) | dialog.returnValue → value of the pressed button, or "cancel" | Native event after any way of closing. Read the form fields here when the value is not cancel. |
cancel (on the dialog) | — | Native event on Esc; preventDefault() to keep unsaved input and ask first. |
Accessibility
- Native modal dialog: focus moves inside, is trapped, Esc closes, focus returns to the opener.
- Named by its
h2(aria-labelledby); the ✕ button has an accessible name. - Cancel buttons use
formnovalidate, so required fields never block closing. - If the dialog contains unsaved input, ask before discarding it on Esc/backdrop ("Änderungen verwerfen?").
Guidance
Related: Confirm dialog · Bottom sheet · Button · Form field