Dialogs
components specs/components/dialogs.kmd
Modal interruption surface — basic dialog (centered modal), full-screen dialog (mobile/onboarding), and alert dialog (destructive confirm). Material parity (`/components/dialogs`). Includes scrim, focus trap, ESC handling, and accessibility contract.
Quando esta spec se aplica
Triggers primários
- Show modal interruption
Todos os triggers
- Need to interrupt user flow with critical decision
- Confirm a destructive action
- Multi-step modal flow on mobile (full-screen variant)
Corpo da especificação
Spec — Dialogs
Facet Visual do Koder Design. Material parity: https://m3.material.io/components/dialogs.
3 dialog variants
| Variant | Visual | Use |
|---|---|---|
| Basic | Centered modal, max-width 560 px, surface bg, elevation 5 | Standard confirm / settings dialog |
| Full-screen | Edge-to-edge surface, app bar with close button | Mobile multi-step flow, immersive picker |
| Alert | Basic dialog with destructive icon + 2 buttons (Cancel + destructive) | Destructive confirmation |
Anatomy (basic)
┌──────────────────────────────────┐
│ [icon?] Title │ ← title row
│ │
│ Body content — explanation │ ← body
│ of the decision the user faces. │
│ │
│ [Cancel] [Action] │ ← actions, right-aligned
└──────────────────────────────────┘
Scrim overlay (page bg darkened)
- Container: radius-lg (16 px default), surface bg, elevation 5
- Padding: 24 px all sides
- Icon (optional): 24 px above title, accent color
- Title:
headline-small(24/32, weight 600) - Body:
body-medium(14/20) - Actions: button row, right-aligned (or left in RTL), 8 px gap
R1 — Scrim
The semi-transparent overlay behind the dialog.
- Background:
rgba(0, 0, 0, 0.5)(light mode) orrgba(0, 0, 0, 0.7)(dark) - Click on scrim: dismisses dialog (unless
nonDismissible: true) - ESC key: dismisses dialog (unless
nonDismissible: true) - Click on scrim is equivalent to clicking "Cancel" action
Per interaction/states.kmd: animate scrim fade-in (medium duration,
decelerate easing) and dialog scale 0.95→1.0 in parallel.
R2 — Full-screen variant
Used on Compact class (per window-size-classes.kmd) or for
multi-step flows.
┌──────────────────────────────────┐
│ [X] Title [✓] │ ← app bar w/ close + confirm
├──────────────────────────────────┤
│ │
│ Content fills viewport │
│ │
│ │
└──────────────────────────────────┘
- App bar at top with close button (left) + confirm action (right)
- Content fills below app bar
- No scrim (it IS the viewport)
- Back navigation per
navigation/back-behavior.kmd(back button = dismiss)
R3 — Alert dialog (destructive)
Basic dialog variant specifically for destructive confirms.
- Icon: warning or error glyph in
errorcolor - Title: question form ("Delete 12 messages?")
- Body: consequences ("This cannot be undone.")
- Actions: Cancel (text variant, default focus) + Delete (text or outlined w/
errorcolor) - Destructive action is RIGHT-aligned (Material convention; some guidelines prefer left — Koder follows right)
Default focus: Cancel button (less destructive); Delete requires deliberate tab/click. Prevents accidental confirm on keyboard "Enter".
R4 — Focus trap
When dialog is open:
- Focus moves into the dialog (first focusable element OR explicitly designated)
- Tab cycles within dialog only — can't tab to background
- Shift+Tab cycles backward within dialog
- On dismiss: focus returns to the element that opened the dialog
Per koder_kit (Flutter): Dialog widget handles this automatically.
Per koder_web_kit: needs explicit focus trap implementation.
R5 — Lifecycle hooks
| Phase | Event |
|---|---|
| Pre-open | onWillOpen — can cancel by returning false |
| Open animation start | onOpening |
| Open animation end | onOpened — first focus applied |
| Action clicked | onAction(actionId) — return true to dismiss; false to keep open |
| Pre-dismiss | onWillClose — can cancel |
| Close animation start | onClosing |
| Close animation end | onClosed — focus returned to opener |
R6 — Title + body content rules
Per foundations/ux-writing.kmd:
- Title: question form for confirms, statement for info ("Saved.")
- Body: max 200 chars typically; explain the WHY or consequences
- Avoid jargon, avoid hedging ("might", "could possibly")
- Use direct voice ("Your changes are saved.")
R7 — Forbidden patterns
- ❌ Dialog with > 3 actions (use sheet/bottom-sheet instead)
- ❌ Nested dialogs (one at a time — second one replaces first)
- ❌ Auto-dismissing dialog after timer (use snackbar)
- ❌ Modal for non-critical info (use snackbar or tooltip)
- ❌ Dialog with form > 1 short input (use full-screen variant or dedicated page)
- ❌ Disabling close button as the ONLY exit (always provide Cancel/X)
R8 — Accessibility
role="dialog"+aria-modal="true"aria-labelledbypointing to title element idaria-describedbypointing to body element id (optional but helpful)- Title is
<h2>semantic - ESC key handler bound
- Focus visible on initial focus + on all interactive elements
- Screen reader announces title + body on open
R9 — Per-preset variation
| Preset | Treatment |
|---|---|
material3 | Elevation 5 shadow + radius-lg |
windows_11 | Mica backdrop instead of plain bg |
windows_95 | 3D bevel border + sharp corners + system title bar |
ios_cupertino | radius-xl (20 px) + ultra-light blur scrim |
glassmorphism | Translucent surface + backdrop blur |
brutalist | Sharp corners + thick border + no shadow |
terminal_classic | Text-only with ASCII box drawing |
Cross-link
foundations/ux-writing.kmd— title + body voicenavigation/back-behavior.kmd— back/ESC dismissalthemes/motion.kmd— entry/exit transitionsthemes/elevation.kmd— level 5 defaulterrors/user-facing-messages.kmd— error dialog content
Requirements (testable)
Requirement: Scrim dismissal {#req-dialogs-scrim}
The dialog scrim SHALL be a semi-transparent overlay behind the dialog that dismisses the dialog on click — equivalent to invoking the "Cancel" action — and on ESC keypress, unless the dialog is configured nonDismissible: true, in which case neither scrim-click nor ESC SHALL dismiss it.
Scenario: Scrim click dismisses a dismissible dialog
- GIVEN a basic dialog is open with
nonDismissibleunset (default) - WHEN the user clicks on the scrim overlay outside the dialog container
- THEN the dialog dismisses
- AND the outcome is equivalent to invoking the "Cancel" action
Scenario: ESC dismisses a dismissible dialog
- GIVEN a basic dialog is open with
nonDismissibleunset - WHEN the user presses the ESC key
- THEN the dialog dismisses
Scenario: Non-dismissible dialog ignores scrim and ESC
- GIVEN a dialog is open with
nonDismissible: true - WHEN the user clicks the scrim or presses ESC
- THEN the dialog remains open
Requirement: Alert dialog default focus {#req-dialogs-alert-focus}
An alert (destructive) dialog SHALL place initial focus on the less-destructive "Cancel" action rather than the destructive action, so that a keyboard "Enter" immediately after open does not trigger the destructive confirm.
Scenario: Cancel holds default focus on open
- GIVEN an alert dialog with Cancel and a destructive Delete action
- WHEN the alert dialog finishes opening
- THEN focus is on the Cancel button
- AND the destructive Delete action is not the focused element
Scenario: Enter on open does not confirm destruction
- GIVEN an alert dialog has just opened with default focus on Cancel
- WHEN the user presses Enter without tabbing
- THEN the destructive action is not invoked
- AND the dialog behaves as if Cancel was chosen
Requirement: Focus trap and focus restoration {#req-dialogs-focus-trap}
While a dialog is open it SHALL trap focus within its own content — Tab and Shift+Tab cycling only among the dialog's focusable elements and never reaching background content — and on dismiss it SHALL return focus to the element that opened the dialog.
Scenario: Tab cycles within the dialog only
- GIVEN a dialog is open with multiple focusable elements
- WHEN the user presses Tab past the last focusable element in the dialog
- THEN focus moves to the first focusable element in the dialog
- AND focus never lands on any element in the background page
Scenario: Focus returns to the opener on dismiss
- GIVEN a control opened the dialog and the dialog is now open
- WHEN the dialog is dismissed
- THEN focus returns to the control that opened the dialog
Requirement: Accessibility semantics {#req-dialogs-a11y}
A dialog SHALL expose role="dialog" with aria-modal="true", reference its title element via aria-labelledby, render its title as a semantic <h2>, and bind an ESC key handler, so assistive technology announces the dialog and users can exit via keyboard.
Scenario: Dialog exposes required ARIA attributes
- GIVEN a dialog is rendered and open
- WHEN the accessibility tree is inspected
- THEN the dialog container has
role="dialog"andaria-modal="true" - AND
aria-labelledbyreferences the id of the title element - AND the title element is a semantic
<h2>
Scenario: Screen reader announces title and body on open
- GIVEN a screen reader is active
- WHEN a dialog opens
- THEN the title and body content are announced
Requirement: Full-screen variant chrome {#req-dialogs-fullscreen}
The full-screen dialog variant SHALL present a top app bar containing a close button on the leading side and a confirm action on the trailing side, fill the viewport with content below the app bar, render no scrim, and dismiss on back navigation.
Scenario: App bar exposes close and confirm
- GIVEN a full-screen dialog is open
- WHEN its app bar is inspected
- THEN a close button is present on the leading (left) side
- AND a confirm action is present on the trailing (right) side
- AND no scrim overlay is rendered
Scenario: Back navigation dismisses the full-screen dialog
- GIVEN a full-screen dialog is open
- WHEN the user triggers back navigation
- THEN the dialog dismisses
Requirement: Forbidden dialog patterns {#req-dialogs-forbidden}
A dialog SHALL NOT present more than 3 actions, SHALL NOT open a second dialog nested over an existing one (a new dialog replaces the current one), and SHALL NOT auto-dismiss on a timer.
Scenario: More than three actions is rejected
- GIVEN a dialog configuration specifying 4 actions
- WHEN the dialog is validated or built
- THEN it is rejected as invalid for exceeding 3 actions
Scenario: Opening a second dialog replaces the first
- GIVEN a dialog is already open
- WHEN a request to open a second dialog is issued
- THEN the first dialog is closed
- AND only one dialog is visible at a time
Scenario: No timer-based auto-dismiss
- GIVEN a dialog is open with no user interaction
- WHEN time elapses beyond any animation duration
- THEN the dialog remains open
Referências
specs/foundations/elements.kmdspecs/foundations/ux-writing.kmdspecs/navigation/back-behavior.kmdspecs/themes/motion.kmdspecs/themes/elevation.kmd