Dialog

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

Trainingsgruppe ändern

Lea Müller wechselt ab dem nächsten Training in die neue Gruppe.

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

Ablauf der Gurtprüfung

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.

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.

FieldTypeRequiredDefaultDescription
idstringrequired—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-]*$
titlestringrequired—Heading naming the task ("Trainingsgruppe ändern").
min 1 chars
paragraphsarray of stringoptional—Body text, one string per paragraph. Linked as the dialog's description.
fieldsarray of objectsoptional—Form fields in the body (see Form field). Their values are in the form when the dialog closes.
fields[].idstringrequired—Id of the control; also prefix for {id}-hint and {id}-error. Must be unique on the page.
pattern ^[A-Za-z][\w-]*$
fields[].labelstringrequired—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[].namestringoptional—Form field name; defaults to id.
fields[].valuestring | numberoptional—Current value.
fields[].placeholderstringoptional—Example value ("z. B. 3011"). Never a substitute for the label.
fields[].hintstringoptional—Help text shown between label and control.
fields[].errorstringoptional—Error message; when set the control is marked invalid. Say what to do: "Bitte gib … ein".
fields[].requiredbooleanoptionalfalseField must be filled. Optional fields are marked "(optional)" instead of marking required ones.
fields[].optionalbooleanoptionalfalseAppend "(optional)" to the label (use in forms where most fields are required).
fields[].disabledbooleanoptionalfalseRead-only for now; explain why in the hint.
fields[].autocompletestringoptional—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[].maxlengthintegeroptional—Maximum characters.
≥ 1
fields[].rowsintegeroptional4Visible lines (control = textarea).
≥ 2
fields[].optionsarray of objectsoptional—Choices (control = select). Put a sensible default first or add an empty "Bitte wählen" option.
fields[].options[].valuestringrequired—Submitted value.
fields[].options[].labelstringrequired—Shown text.
fields[].options[].disabledbooleanoptional—Not selectable (e.g. full group).
size"md" | "lg"optional"md"md 32 rem wide, lg 48 rem for tables or previews.
closeLabelstringoptional"Schliessen"Accessible name of the ✕ button in the header.
showClosebooleanoptionaltrueShow the ✕ button in the header.
actionsarray of objectsrequired—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[].labelstringrequired—Button text, a verb naming the result ("Gruppe ändern"); "Abbrechen" for the cancel button.
min 1 chars
actions[].valuestringrequired—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.
triggerobjectoptional—Optional opening button rendered before the dialog (data-behavior="open-dialog").
trigger.labelstringrequired—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.iconstringoptional—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

AttributeValuesDefaultDescription
aria-labelledby"{id}-title"—Named by its heading.
aria-describedby"{id}-body"—Body text as description (only when there are paragraphs).
data-sizelg(none = md)Width.

Parts

PartDescription
.dialog__inner<form method="dialog"> wrapping everything.
.dialog__headerTitle (h2) and ✕ button (value="cancel").
.dialog__bodyScrollable content.
.dialog__footer.button-rowActions; stacked full width on phones with the primary at the bottom (data-stack-mobile).

States

StateDescription
[open]Shown (with entry animation).
::backdropDimmed 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

FunctionSignatureReturnsDescription
openDialogopenDialog(dialog: HTMLDialogElement, opener?: HTMLElement)voidOpens with showModal(), closes on backdrop click (returnValue = "cancel"), returns focus to opener (default: the active element) on close.

Events

EventDetailDescription
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

DoTitle names the task; the primary button repeats the verb ("Gruppe ändern").
DoKeep it short — one topic, few fields.
Don'tDon't open a dialog from a dialog.
Don'tDon't use a dialog for messages that need no decision — use an alert or toast.
Don'tDon't put a destructive button next to the primary without a confirm.

Related: Confirm dialog · Bottom sheet · Button · Form field