Pular para o conteúdo

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

Todos os triggers

Corpo da especificação

Spec — Dialogs

Facet Visual do Koder Design. Material parity: https://m3.material.io/components/dialogs.

3 dialog variants

VariantVisualUse
BasicCentered modal, max-width 560 px, surface bg, elevation 5Standard confirm / settings dialog
Full-screenEdge-to-edge surface, app bar with close buttonMobile multi-step flow, immersive picker
AlertBasic 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) or rgba(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 error color
  • Title: question form ("Delete 12 messages?")
  • Body: consequences ("This cannot be undone.")
  • Actions: Cancel (text variant, default focus) + Delete (text or outlined w/ error color)
  • 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

PhaseEvent
Pre-openonWillOpen — can cancel by returning false
Open animation startonOpening
Open animation endonOpened — first focus applied
Action clickedonAction(actionId) — return true to dismiss; false to keep open
Pre-dismissonWillClose — can cancel
Close animation startonClosing
Close animation endonClosed — 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-labelledby pointing to title element id
  • aria-describedby pointing 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

PresetTreatment
material3Elevation 5 shadow + radius-lg
windows_11Mica backdrop instead of plain bg
windows_953D bevel border + sharp corners + system title bar
ios_cupertinoradius-xl (20 px) + ultra-light blur scrim
glassmorphismTranslucent surface + backdrop blur
brutalistSharp corners + thick border + no shadow
terminal_classicText-only with ASCII box drawing
  • foundations/ux-writing.kmd — title + body voice
  • navigation/back-behavior.kmd — back/ESC dismissal
  • themes/motion.kmd — entry/exit transitions
  • themes/elevation.kmd — level 5 default
  • errors/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 nonDismissible unset (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 nonDismissible unset
  • 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" and aria-modal="true"
  • AND aria-labelledby references 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