Skip to content

Dialog

A modal that interrupts to get a decision. Basic or full-screen, with an optional icon and headline, a scrim, focus trapping, and an actions row you populate yourself.

Live preview Open in Storybook
Open dialog

Your changes will be lost.

Cancel Discard
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-button variant="filled">Open dialog</md-button>
<md-dialog id="demo-dialog-basic" headline="Discard draft?">
  <p style="margin: 0;">Your changes will be lost.</p>
  <md-button slot="actions" variant="text">Cancel</md-button>
  <md-button slot="actions" variant="filled">Discard</md-button>
</md-dialog>

<script type="module">
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dialog-basic').show());
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dialog-basic').close());
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dialog-basic').close());
</script>

Already installed? See the Installation guide for one-time package setup (core + tokens, fonts). Each tab below shows two patterns for using md-dialog in your project: Option A registers every AWC UI component at once (simplest), Option B imports only this component for tree-shake-friendly bundles.

<!-- ─── Option A: global registration (all components) ─── -->
<script type="module">
  import '@awc-ui/core/define';
</script>


<!-- ─── Option B: single import (tree-shake only md-dialog) ─── -->
<script type="module">
  import '@awc-ui/core/components/md-dialog';
</script>


<md-dialog></md-dialog>
  • A prompt that blocks normal operation and needs a decision, an acknowledgement, or a specific task.
  • Critical information the user must not miss.
  • Confirming a destructive or irreversible action.
  • A focused sub-task on mobile (fullscreen).
SituationUse instead
Low- or medium-priority informationmd-snackbar
A brief confirmation of something that already happenedmd-snackbar
Explaining a controlmd-tooltip
Supplementary content, mobilemd-bottom-sheet
Supplementary content, desktopmd-side-sheet
A list of actions from a triggermd-menu
Content that could just live on the pageA page or md-card
Field-level validation errorsInline error text on the field

You supply the buttons through slot="actions". cancel-label and ok-label name the built-in fallback buttons that appear when you slot nothing; slotted buttons win, so don’t set both and expect them to merge.

M3 also wants confirming actions disabled until a choice is made, while dismissive actions are never disabled. That is your logic, not the component’s.

Slotted actions versus the built-in fallback pair Open in Storybook
Slotted actions Fallback actions

Both buttons are mine. Dismissive first in DOM order, confirming second — the component renders them exactly as written.

Cancel Discard

Nothing is slotted into actions, so the built-in pair renders instead — named by cancel-label and ok-label. Both close the dialog; the cancel one also fires mdCancel.

Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<!-- You supply the pair: dismissive first, confirming second -->
<md-dialog headline="Discard draft?">
  <p>Your changes will be lost.</p>
  <md-button slot="actions" variant="text">Cancel</md-button>
  <md-button slot="actions" variant="filled">Discard</md-button>
</md-dialog>

<!-- Nothing slotted: the built-in pair renders, named by cancel-label / ok-label -->
<md-dialog headline="Leave without saving?" cancel-label="Stay" ok-label="Leave">
  <p>Your changes will be lost.</p>
</md-dialog>

An icon sits above the headline and centre-aligns the header. Without one the header is start-aligned.

The basic-dialog story is the same shape without the icon.

Icon header, and an acknowledgement-only dialog Open in Storybook
Delete permanently

This cannot be undone.

Cancel Delete
Show update notice

Version 2.4 is now active.

Got it
Show code for each technology
<!-- index.html <head> — the icon font the components draw from -->
<link rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">

<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-button variant="tonal">Delete permanently</md-button>
<md-dialog id="demo-dialog-icon" headline="Delete 3 files?" icon="delete">
  <p style="margin: 0;">This cannot be undone.</p>
  <md-button slot="actions" variant="text">Cancel</md-button>
  <md-button slot="actions" variant="filled">Delete</md-button>
</md-dialog>
<md-button variant="tonal">Show update notice</md-button>
<md-dialog id="demo-dialog-ack" headline="Update installed" icon="check_circle">
  <p style="margin: 0;">Version 2.4 is now active.</p>
  <md-button slot="actions" variant="text">Got it</md-button>
</md-dialog>

<script type="module">
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dialog-icon').show());
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dialog-icon').close());
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dialog-icon').close());
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dialog-ack').show());
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dialog-ack').close());
</script>

A single action is only right when it is an acknowledgement. “Cancel” makes no sense when nothing was proposed.

icon takes a Material Symbols name; the icon slot replaces it when you need a custom glyph or an SVG.

An SVG in the icon slot Open in Storybook
Slotted icon

Every panel returns to its default position. Saved layouts are untouched.

Cancel Reset
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-button variant="tonal">Slotted icon</md-button>

<md-dialog id="demo-dialog-svgicon" headline="Reset your workspace?">
  <svg slot="icon" viewBox="0 0 24 24" width="24" height="24" fill="currentColor" aria-hidden="true"><path d="M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z"/></svg>
  <p style="margin: 0;">Every panel returns to its default position. Saved layouts are untouched.</p>
  <md-button slot="actions" variant="text">Cancel</md-button>
  <md-button slot="actions" variant="filled">Reset</md-button>
</md-dialog>

<script type="module">
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dialog-svgicon').show());
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dialog-svgicon').close());
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dialog-svgicon').close());
</script>

scrim-dismissible defaults to true — clicking the scrim closes the dialog. Turn it off for destructive confirmations, where an accidental click-away shouldn’t decide anything.

Scrim dismissal turned off Open in Storybook
Reset account

Every setting returns to its default. Clicking outside will not dismiss this dialog.

Cancel Reset
Show code for each technology
<!-- index.html <head> — the icon font the components draw from -->
<link rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">

<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-button variant="filled">Reset account</md-button>
<md-dialog id="demo-dialog-strict" headline="Reset this account?" icon="warning" scrim-dismissible="false">
  <p style="margin: 0;">Every setting returns to its default. Clicking outside will not dismiss this dialog.</p>
  <md-button slot="actions" variant="text">Cancel</md-button>
  <md-button slot="actions" variant="filled">Reset</md-button>
</md-dialog>

<script type="module">
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dialog-strict').show());
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dialog-strict').close());
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dialog-strict').close());
</script>

divider draws a rule between the content area and the actions row, on either variant. header-divider draws the matching rule under the header — and it is full-screen only: the component gates it on fullscreen, so on a basic dialog the attribute is inert. Both earn their place when the body scrolls; without them a scrolled body runs visually into the headline and the actions row.

Content rule on a basic dialog, header plus content rules on a full-screen one Open in Storybook
Open terms (basic) Open terms (full-screen)

A basic dialog takes divider only — the rule sits above the actions row.

Section 1. Lorem ipsum dolor sit amet, consectetur adipiscing elit.

Section 2. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Section 3. Ut enim ad minim veniam, quis nostrud exercitation ullamco.

Section 4. Duis aute irure dolor in reprehenderit in voluptate velit.

Decline Accept

Full-screen is where header-divider renders — a rule under the app bar, plus the content rule from divider.

Section 1. Lorem ipsum dolor sit amet, consectetur adipiscing elit.

Section 2. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Section 3. Ut enim ad minim veniam, quis nostrud exercitation ullamco.

Decline Accept
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-button variant="outlined">Open terms (basic)</md-button>
<md-button variant="outlined">Open terms (full-screen)</md-button>

<md-dialog id="demo-dialog-divider" headline="Terms of service" divider>
  <p style="margin: 0 0 12px;">A basic dialog takes <code>divider</code> only — the rule sits above the actions row.</p>
  <p style="margin: 0 0 12px;">Section 1. Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
  <p style="margin: 0 0 12px;">Section 2. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
  <p style="margin: 0 0 12px;">Section 3. Ut enim ad minim veniam, quis nostrud exercitation ullamco.</p>
  <p style="margin: 0;">Section 4. Duis aute irure dolor in reprehenderit in voluptate velit.</p>
  <md-button slot="actions" variant="text">Decline</md-button>
  <md-button slot="actions" variant="filled">Accept</md-button>
</md-dialog>

<md-dialog id="demo-dialog-divider-full" fullscreen headline="Terms of service" close-label="Close" header-divider divider>
  <p style="margin: 0 0 12px;">Full-screen is where <code>header-divider</code> renders — a rule under the app bar, plus the content rule from <code>divider</code>.</p>
  <p style="margin: 0 0 12px;">Section 1. Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
  <p style="margin: 0 0 12px;">Section 2. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
  <p style="margin: 0;">Section 3. Ut enim ad minim veniam, quis nostrud exercitation ullamco.</p>
  <md-button slot="actions" variant="text">Decline</md-button>
  <md-button slot="actions" variant="filled">Accept</md-button>
</md-dialog>

<script type="module">
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dialog-divider').show());
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dialog-divider-full').show());
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dialog-divider').close());
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dialog-divider').close());
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dialog-divider-full').close());
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dialog-divider-full').close());
</script>

fullscreen is the mobile sub-task form: an app bar with the headline, a close button, and the body filling the viewport.

Full-screen dialog Open in Storybook
New event

Create a calendar event

Long headlines belong here in the body, not in the app bar.

Cancel Create
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-button variant="filled" id="new-event">New event</md-button>

<md-dialog id="event" fullscreen headline="New event" close-label="Close">
  <h2>Create a calendar event</h2>
  <md-text-field label="Title"></md-text-field>
  <md-button slot="actions" variant="text" data-close>Cancel</md-button>
  <md-button slot="actions" variant="filled" disabled>Create</md-button>
</md-dialog>

<script type="module">
  const dialog = document.getElementById('event');

  document.getElementById('new-event').addEventListener('mdClick', () => dialog.show());
  dialog.querySelectorAll('[data-close]').forEach((b) =>
    b.addEventListener('mdClick', () => dialog.close()),
  );
</script>

open is two-way: assign it to drive the dialog from your own state, and read it back after a user dismissal. show() / close() do the same thing imperatively — pick one and stay with it, because mixing them is how state ends up disagreeing with what is on screen. To mirror the state elsewhere in your UI, listen for mdOpen / mdClose (see Events) rather than polling the property; a scrim dismissal or Escape changes it without going through your button.

Opened and closed through the open prop Open in Storybook
Open by setting open = true

This dialog was opened by assigning open, not by calling show() — and the action below closes it the same way.

Set open = false
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-button variant="filled" onclick="document.getElementById('demo-dialog-ctl').open = true">Open by setting open = true</md-button>

<md-dialog id="demo-dialog-ctl" headline="Driven by the prop">
  <p style="margin: 0;">This dialog was opened by assigning <code>open</code>, not by calling <code>show()</code> — and the action below closes it the same way.</p>
  <md-button slot="actions" variant="text" onclick="document.getElementById('demo-dialog-ctl').open = false">Set open = false</md-button>
</md-dialog>
EventCancelableDetailFires
mdOpennovoidThe dialog opened
mdClosenovoidThe dialog closed, for any reason
mdCancelnovoidThe user dismissed it — Escape, scrim click, or the close button

They overlap on purpose. Every dismissal fires both, in this order: mdCancel, then mdClose. mdClose is the superset — it also covers your own close() call and an open = false assignment. mdCancel is the narrow one that means the user backed out, and it is the only way to tell a dismissal apart from a confirmation, because the component itself doesn’t know which of your slotted buttons meant what.

Neither event is cancelable, so neither can veto a close. They report; they don’t gate.

You want to…Listen to
Tear down or resync whenever the dialog goes awaymdClose
Mirror open back into your own statemdClose
Treat “backed out” differently from “confirmed”mdCancel
Log an explicit dismissal — Escape, scrim, close buttonmdCancel
Run something once per dismissalmdCancel alone — not both
Block a closeNeither. Set scrim-dismissible="false"; Escape always closes

Methodsshow() and close() drive the dialog imperatively; open is the declarative equivalent.

All three events bubble and are composed, so a single listener on the dialog — or on an ancestor of it — catches every one. Open the dialog below and close it four different ways; the log records what fired.

Every close path, logged — mdCancel only fires for a dismissal
Open the dialog Clear log

Close me with Discard, with Cancel, with Escape, or by clicking the scrim — then read the log underneath.

Cancel Discard
Nothing yet. Open the dialog, then dismiss it.
Show code for each technology
<md-button id="open" variant="filled">Open the dialog</md-button>

<md-dialog id="confirm" headline="Discard draft?" icon="delete">
<p>Your changes will be lost.</p>
<md-button slot="actions" variant="text" data-close>Cancel</md-button>
<md-button slot="actions" variant="filled" data-close>Discard</md-button>
</md-dialog>

<script type="module">
const dialog = document.getElementById('confirm');

document.getElementById('open').addEventListener('mdClick', () => dialog.show());
dialog.querySelectorAll('[data-close]').forEach((b) =>
  b.addEventListener('mdClick', () => dialog.close()),
);

dialog.addEventListener('mdOpen', () => console.log('opened'));
dialog.addEventListener('mdCancel', () => console.log('dismissed'));
dialog.addEventListener('mdClose', () => console.log('closed'));
</script>

That same wiring inside a real destructive confirmation — click-away turned off, open owned by your state, and the discard branch doing actual work:

<md-button id="trigger" variant="filled">Discard draft</md-button>

<md-dialog id="confirm" headline="Discard draft?" icon="delete" scrim-dismissible="false">
<p>Your changes will be lost.</p>
<md-button slot="actions" variant="text" id="cancel">Cancel</md-button>
<md-button slot="actions" variant="filled" id="discard">Discard</md-button>
</md-dialog>

<script type="module">
const dlg = document.getElementById('confirm');

document.getElementById('trigger').addEventListener('click', () => dlg.show());
document.getElementById('cancel').addEventListener('mdClick', () => dlg.close());
document.getElementById('discard').addEventListener('mdClick', () => {
  discardDraft();
  dlg.close();
});

dlg.addEventListener('mdCancel', () => console.log('dismissed'));
dlg.addEventListener('mdClose', () => restoreFocusToTrigger());
</script>

Properties

PropertyAttributeTypeDefaultReflects
openopenbooleanfalseYes
headlineheadlinestring''
iconiconstring''
fullscreenfullscreenbooleanfalseYes
scrimDismissiblescrim-dismissiblebooleantrue
localelocalestring'en-US'
closeLabelclose-labelstring''
cancelLabelcancel-labelstring''
okLabelok-labelstring''
dividerdividerbooleanfalseYes
headerDividerheader-dividerbooleanfalseYes
densitydensity0 | -1 | -2 | -3 | -40Yes

Methods

MethodParameters
show()none
close()none

Slots

SlotDescription
(default)
actionsAction buttons
header-actionFull-screen header action (e.g. Save button)
iconCustom icon (replaces icon prop)
headlineCustom headline content

CSS Custom Properties

Override on the host element for per-instance theming:

PropertyDescription
--md-dialog-container-colorContainer background
--md-dialog-container-shapeContainer corner radius
--md-dialog-headline-colorHeadline text color
--md-dialog-content-colorSupporting text color
--md-dialog-icon-colorIcon color
--md-dialog-icon-sizeIcon dimensions
--md-dialog-scrim-colorScrim overlay color
--md-dialog-divider-colorDivider color
--md-dialog-z-indexSurface z-index (default near int32 max)
--md-dialog-scrim-z-indexScrim z-index (one below the surface)

CSS Shadow Parts

Style internal elements through shadow DOM with ::part():

PartDescription
actionsAction buttons row
cancel-buttonDefault cancel button (slot fallback)
ok-buttonDefault confirm button (slot fallback)
scrimScrim overlay (basic only)
containerDialog surface
headerHeader area (both variants)
close-buttonClose button (full-screen)
headlineHeadline text
dividerHorizontal divider
iconIcon element (basic)
contentSupporting text / body
  • The dialog is modal: focus is trapped while it is open, content behind the scrim is inert, and Escape dismisses (firing mdCancel). Don’t try to keep background controls reachable.
  • Focus restoration is built in. The dialog records the active element when it opens and refocuses it on every close, with preventScroll so the page never jumps. You don’t need an mdClose handler for it — use mdClose for whatever else the surrounding UI has to resync.
  • headline supplies the accessible name. If you slot a custom headline, make sure the dialog still ends up with one.
  • On a fullscreen dialog, give the app-bar close button a localized name via close-label. A basic dialog has no close button, so the prop does nothing there.
  • Keep the action order logical for keyboard users as well as visually — DOM order is what screen readers follow, and it is also what decides the visual order.

Open this one and hold Tab down: focus cycles through the field and the two actions and never reaches the page behind the scrim. Press Escape and focus lands back on the button you pressed — the component does that itself.

Focus trap, Escape to dismiss, focus returned to the trigger Open in Storybook
Open, then press Tab

The field and both buttons are the whole tab cycle. Nothing behind the scrim is reachable.

Cancel Rename
Show code for each technology
<!-- index.html <head> — the icon font the components draw from -->
<link rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">

<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-button variant="filled" id="rename">Rename project</md-button>

<md-dialog id="rename-dialog" headline="Rename project" icon="edit">
  <md-text-field label="Project name" value="Untitled"></md-text-field>
  <md-button slot="actions" variant="text" data-close>Cancel</md-button>
  <md-button slot="actions" variant="filled" data-close>Rename</md-button>
</md-dialog>

<script type="module">
  const trigger = document.getElementById('rename');
  const dialog = document.getElementById('rename-dialog');

  trigger.addEventListener('mdClick', () => dialog.show());
  dialog.querySelectorAll('[data-close]').forEach((b) =>
    b.addEventListener('mdClick', () => dialog.close()),
  );

  // Focus is restored to the opener for you. Use mdClose for anything else
  // the surrounding UI needs to resync after any close.
  dialog.addEventListener('mdClose', () => refreshProjectList());
</script>

The trap reaches into slotted custom elements: the <input> inside md-text-field is a real tab stop even though it lives behind a shadow boundary. role follows the variant — a basic dialog is an alertdialog, a fullscreen one is a plain dialog.

The accessibility story walks the focus trap, the restored focus target and the announced role, and responsiveness covers narrow viewports.

RTL — the container and the actions row mirror under dir="rtl". “Dismissive on the left” means leading, so it lands on the right in RTL automatically. Don’t hardcode a side. See RTL.

<md-dialog dir="rtl" headline="حذف الملف؟" icon="delete">
<md-button slot="actions" variant="text">إلغاء</md-button>
<md-button slot="actions" variant="filled">حذف</md-button>
</md-dialog>
Same markup, dir=ltr vs dir=rtl Open in Storybook
ltr
Delete file
rtl
حذف الملف

This cannot be undone.

Cancel Delete

لا يمكن التراجع عن هذا الإجراء.

إلغاء حذف
Show code for each technology
<!-- index.html <head> — the icon font the components draw from -->
<link rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">

<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<div style="display:grid;grid-template-columns:auto 1fr;gap:14px 16px;align-items:center;">
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">ltr</span>
  <div style="display:flex;flex-wrap:wrap;gap:12px;">
    <md-button variant="filled">Delete file</md-button>
  </div>
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">rtl</span>
  <div dir="rtl" style="display:flex;flex-wrap:wrap;gap:12px;">
    <md-button variant="filled">حذف الملف</md-button>
  </div>
</div>

<md-dialog id="demo-dialog-ltr" headline="Delete file?" icon="delete">
  <p style="margin: 0;">This cannot be undone.</p>
  <md-button slot="actions" variant="text">Cancel</md-button>
  <md-button slot="actions" variant="filled">Delete</md-button>
</md-dialog>

<md-dialog id="demo-dialog-rtl" dir="rtl" headline="حذف الملف؟" icon="delete">
  <p style="margin: 0;">لا يمكن التراجع عن هذا الإجراء.</p>
  <md-button slot="actions" variant="text">إلغاء</md-button>
  <md-button slot="actions" variant="filled">حذف</md-button>
</md-dialog>

<script type="module">
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dialog-ltr').show());
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dialog-rtl').show());
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dialog-ltr').close());
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dialog-ltr').close());
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dialog-rtl').close());
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dialog-rtl').close());
</script>

The pair mirrors because the actions row is a logical-direction flex row and your slotted buttons keep their DOM order. Force a physical side — with ::part(actions) and row-reverse, say — and RTL breaks: the confirming action ends up where the dismissive one belongs.

Correct vs hardcoded — both under dir=rtl
correct
DOM order only
wrong
Hardcoded row-reverse

الإجراء الرافض في الجهة الأمامية — تلقائياً.

إلغاء حذف

نفس ترتيب العناصر، لكن الصف مقلوب يدوياً — والنتيجة خاطئة.

إلغاء حذف
Show code for each technology
<!-- index.html <head> — the icon font the components draw from -->
<link rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">

<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<style>
  #demo-dialog-rtl-wrong::part(actions) { flex-direction: row-reverse; }
</style>
<div style="display:grid;grid-template-columns:auto 1fr;gap:14px 16px;align-items:center;">
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">correct</span>
  <div dir="rtl" style="display:flex;flex-wrap:wrap;gap:12px;">
    <md-button variant="filled">DOM order only</md-button>
  </div>
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">wrong</span>
  <div dir="rtl" style="display:flex;flex-wrap:wrap;gap:12px;">
    <md-button variant="outlined">Hardcoded row-reverse</md-button>
  </div>
</div>

<md-dialog id="demo-dialog-rtl-right" dir="rtl" headline="حذف الملف؟" icon="delete">
  <p style="margin: 0;">الإجراء الرافض في الجهة الأمامية — تلقائياً.</p>
  <md-button slot="actions" variant="text">إلغاء</md-button>
  <md-button slot="actions" variant="filled">حذف</md-button>
</md-dialog>

<md-dialog id="demo-dialog-rtl-wrong" dir="rtl" headline="حذف الملف؟" icon="delete">
  <p style="margin: 0;">نفس ترتيب العناصر، لكن الصف مقلوب يدوياً — والنتيجة خاطئة.</p>
  <md-button slot="actions" variant="text">إلغاء</md-button>
  <md-button slot="actions" variant="filled">حذف</md-button>
</md-dialog>

<script type="module">
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dialog-rtl-right').show());
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dialog-rtl-wrong').show());
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dialog-rtl-right').close());
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dialog-rtl-right').close());
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dialog-rtl-wrong').close());
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dialog-rtl-wrong').close());
</script>

density="-1…-4" is a local override of the inherited data-density. The resting state is simply no density attribute — there is no [density="0"] rule to opt back in with (see the caution below). All five steps, same dialog, so the taper — inset, headline size, corner radius and the actions row — is visible in one place:

Default through -4 — inset, headline and actions row all taper
default
Open default
-1
Open rung -1
-2
Open rung -2
-3
Open rung -3
-4
Open rung -4

Inset, headline size, corner radius and the actions row all taper together.

Close

Inset, headline size, corner radius and the actions row all taper together.

Close

Inset, headline size, corner radius and the actions row all taper together.

Close

Inset, headline size, corner radius and the actions row all taper together.

Close

Inset, headline size, corner radius and the actions row all taper together.

Close
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<div style="display:grid;grid-template-columns:auto 1fr;gap:14px 16px;align-items:center;">
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">default</span>
  <div style="display:flex;flex-wrap:wrap;gap:12px;"><md-button variant="filled">Open default</md-button></div>
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">-1</span>
  <div style="display:flex;flex-wrap:wrap;gap:12px;"><md-button variant="outlined">Open rung -1</md-button></div>
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">-2</span>
  <div style="display:flex;flex-wrap:wrap;gap:12px;"><md-button variant="outlined">Open rung -2</md-button></div>
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">-3</span>
  <div style="display:flex;flex-wrap:wrap;gap:12px;"><md-button variant="outlined">Open rung -3</md-button></div>
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">-4</span>
  <div style="display:flex;flex-wrap:wrap;gap:12px;"><md-button variant="outlined">Open rung -4</md-button></div>
</div>

<md-dialog id="demo-dlg-d0" headline="Default density"><p style="margin:0;">Inset, headline size, corner radius and the actions row all taper together.</p><md-button slot="actions" variant="text">Close</md-button></md-dialog>
<md-dialog id="demo-dlg-d1" density="-1" headline="Density -1"><p style="margin:0;">Inset, headline size, corner radius and the actions row all taper together.</p><md-button slot="actions" variant="text">Close</md-button></md-dialog>
<md-dialog id="demo-dlg-d2" density="-2" headline="Density -2"><p style="margin:0;">Inset, headline size, corner radius and the actions row all taper together.</p><md-button slot="actions" variant="text">Close</md-button></md-dialog>
<md-dialog id="demo-dlg-d3" density="-3" headline="Density -3"><p style="margin:0;">Inset, headline size, corner radius and the actions row all taper together.</p><md-button slot="actions" variant="text">Close</md-button></md-dialog>
<md-dialog id="demo-dlg-d4" density="-4" headline="Density -4"><p style="margin:0;">Inset, headline size, corner radius and the actions row all taper together.</p><md-button slot="actions" variant="text">Close</md-button></md-dialog>

<script type="module">
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dlg-d0').show());
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dlg-d1').show());
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dlg-d2').show());
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dlg-d3').show());
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dlg-d4').show());
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dlg-d0').close());
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dlg-d1').close());
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dlg-d2').close());
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dlg-d3').close());
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dlg-d4').close());
</script>

They are independent signals: dir mirrors the layout and data-density compacts it, and a local density prop retunes one dialog without touching direction. All three dialogs below sit in the same dir="rtl" data-density="-1" wrapper: the first inherits -1, the second declares density="-4" and that lower local rung wins, and the third resets to default spacing the only way that works — style="--md-sys-density-scale: 0", not density="0".

dir=rtl with data-density=-1: inherited, overridden to -4, and reset via --md-sys-density-scale
مضغوط (‎-1 موروث) أكثر ضغطاً (‎-4 محلي) إعادة الضبط (متغير مخصص)

يرث الرُتبة ‎-1 من العنصر الأب.

إلغاء حذف

نفس العنصر الأب، لكن الرُتبة المحلية ‎-4 تتغلب عليه.

إلغاء حذف

نفس العنصر الأب، لكن المتغير المخصص يعيد التباعد الافتراضي.

إلغاء حذف
Show code for each technology
<!-- index.html <head> — the icon font the components draw from -->
<link rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">

<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<div dir="rtl" data-density="-1" style="display:flex;flex-wrap:wrap;gap:12px;">
  <md-button variant="filled">مضغوط (‎-1 موروث)</md-button>
  <md-button variant="outlined">أكثر ضغطاً (‎-4 محلي)</md-button>
  <md-button variant="text">إعادة الضبط (متغير مخصص)</md-button>

  <md-dialog id="demo-dlg-rtl-dense" headline="حذف الملف؟" icon="delete">
    <p style="margin:0;">يرث الرُتبة ‎-1 من العنصر الأب.</p>
    <md-button slot="actions" variant="text">إلغاء</md-button>
    <md-button slot="actions" variant="filled">حذف</md-button>
  </md-dialog>

  <md-dialog id="demo-dlg-rtl-denser" density="-4" headline="حذف الملف؟" icon="delete">
    <p style="margin:0;">نفس العنصر الأب، لكن الرُتبة المحلية ‎-4 تتغلب عليه.</p>
    <md-button slot="actions" variant="text">إلغاء</md-button>
    <md-button slot="actions" variant="filled">حذف</md-button>
  </md-dialog>

  <md-dialog id="demo-dlg-rtl-reset" style="--md-sys-density-scale: 0;" headline="حذف الملف؟" icon="delete">
    <p style="margin:0;">نفس العنصر الأب، لكن المتغير المخصص يعيد التباعد الافتراضي.</p>
    <md-button slot="actions" variant="text">إلغاء</md-button>
    <md-button slot="actions" variant="filled">حذف</md-button>
  </md-dialog>
</div>

<script type="module">
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dlg-rtl-dense').show());
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dlg-rtl-denser').show());
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dlg-rtl-reset').show());
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dlg-rtl-dense').close());
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dlg-rtl-dense').close());
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dlg-rtl-denser').close());
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dlg-rtl-denser').close());
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dlg-rtl-reset').close());
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dlg-rtl-reset').close());
</script>

Densitydensity="-1…-4" compacts the padding, the headline and the actions row. A global data-density ancestor drives the same signal, and a lower rung declared on the dialog itself overrides it. See Density.

i18n — translate headline, the body content, close-label, cancel-label and ok-label (the localization story switches the whole set). locale drives any Intl-formatted values the dialog renders. Translated action labels run longer, so check whether the pair still fits side by side or needs stacking (confirming above dismissive).

Custom propertyPurposeDefault
--md-dialog-container-colorSurface fillsurface-container-high
--md-dialog-container-shapeCorner radius28px (0 when fullscreen)
--md-dialog-headline-color / --md-dialog-content-colorTexton-surface / on-surface-variant
--md-dialog-icon-color / --md-dialog-icon-sizeHeader glyphsecondary / 24px
--md-dialog-scrim-colorBackdroprgba(0, 0, 0, 0.32)
--md-dialog-divider-colorHeader and content rulesoutline-variant
--md-dialog-z-index / --md-dialog-scrim-z-indexStacking2147483647 / 2147483646
Themed instance Open in Storybook
Open themed dialog

8px corners, an error-tinted icon and a heavier scrim.

Close
Show code for each technology
<!-- index.html <head> — the icon font the components draw from -->
<link rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">

<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-button variant="outlined">Open themed dialog</md-button>
<md-dialog id="demo-dialog-themed" headline="Themed" icon="palette" style="--md-dialog-container-shape: 8px; --md-dialog-icon-color: var(--md-sys-color-error); --md-dialog-scrim-color: rgba(0, 0, 0, 0.6);">
  <p style="margin: 0;">8px corners, an error-tinted icon and a heavier scrim.</p>
  <md-button slot="actions" variant="text">Close</md-button>
</md-dialog>

<script type="module">
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dialog-themed').show());
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dialog-themed').close());
</script>

Check any override in both themes — the scrim sits at a different opacity in each, so a container colour that separates cleanly on light can disappear into the backdrop on dark. See the dark-theme story.

CSS partscontainer, header, headline, content, divider and actions render on both variants. The rest are conditional: scrim and icon are basic-only (a full-screen dialog has neither), close-button is full-screen only, and cancel-button / ok-button exist only while nothing is slotted into actions. Styling a part the current variant doesn’t render is a silent no-op.

::part() overrides
Styled parts

Outlined container, italic headline, tinted scrim and a wider action gap.

Cancel Apply
Show code for each technology
<!-- index.html <head> — the icon font the components draw from -->
<link rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">

<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<style>
  #demo-dialog-parts::part(container) { border: 2px solid var(--md-sys-color-primary); max-inline-size: 420px; }
  #demo-dialog-parts::part(headline) { font-style: italic; letter-spacing: .04em; }
  #demo-dialog-parts::part(scrim) { background: color-mix(in srgb, var(--md-sys-color-primary) 28%, transparent); }
  #demo-dialog-parts::part(actions) { gap: 16px; }
</style>
<md-button variant="filled">Styled parts</md-button>

<md-dialog id="demo-dialog-parts" headline="Styled with ::part()" icon="palette">
  <p style="margin: 0;">Outlined container, italic headline, tinted scrim and a wider action gap.</p>
  <md-button slot="actions" variant="text">Cancel</md-button>
  <md-button slot="actions" variant="filled">Apply</md-button>
</md-dialog>

<script type="module">
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dialog-parts').show());
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dialog-parts').close());
  // Give the trigger an id, then:
  // trigger.addEventListener('click', () => document.getElementById('demo-dialog-parts').close());
</script>
md-dialog::part(container) {
max-inline-size: 420px;
}

md-snackbar · md-bottom-sheet · md-side-sheet · md-menu · md-card · md-button · md-tooltip

For AI Agents — md-dialog

Two artefacts to give your AI agent so it generates correct UI with this component. The per-component spec answers "how do I use this exact tag?". The main-llm spec answers "which tag should I pick in the first place?".

Per-component

md-dialog spec card

Identity · when to use / when NOT · decision cues · behavioural contract · do/don't · anti-patterns · full API. Paste into your agent when you're implementing with this component.

Main-LLM spec

AWC UI Operator's Manual

System-prompt preamble · decision matrix · token reference · page recipes · anti-patterns. Paste into the system prompt at the start of a piece of work.

Open spec

md-dialog readme.md

# md-dialog

<!-- llm:meta
tag: md-dialog
category: containment
status: md3-mapped
m3-guidelines: https://m3.material.io/components/dialogs/guidelines
form-associated: false
depends-on: md-button
used-by: none
-->

**A modal that interrupts to get a decision.** Basic or full-screen, with an
optional icon and headline, a scrim, a focus trap that reaches into slotted
shadow roots, body-scroll locking, and an actions row you normally slot
yourself.

> Setup, theming, density and i18n are configured once for the whole library —
> see the library-wide specification, shipped next to these manuals as
> `main-llm.md` at the root of the `@awc-ui/core` package.

---

## When to use

- A prompt that **blocks normal operation** and needs a decision,
  acknowledgement, or a specific task.
- **Critical** information the user must not miss.
- Confirming a destructive or irreversible action ("Discard unsaved changes?").
- A focused sub-task on mobile (`fullscreen`).

## When NOT to use

| Situation | Use instead |
|---|---|
| Low- or medium-priority information | `md-snackbar` |
| A brief confirmation of something that already happened | `md-snackbar` |
| Explaining a control | `md-tooltip` |
| Supplementary content, mobile | `md-bottom-sheet` |
| Supplementary content, desktop | `md-side-sheet` |
| A list of actions from a trigger | `md-menu` |
| Content that could just live on the page | A page or `md-card` |
| Field-level validation errors | Inline error text on the field |

## Decision cues

| Need | Setting |
|---|---|
| Standard modal | default (no `fullscreen`) |
| Immersive sub-task (mobile) | `fullscreen` |
| Prevent click-away dismissal (destructive flows) | `scrim-dismissible="false"` |
| Emphasis glyph above the headline | `icon="warning"` or `slot="icon"` (basic variant only) |
| Rich headline markup | `slot="headline"` instead of the `headline` prop (basic variant only) |
| A title on a `fullscreen` dialog | The `headline` **prop** — the bar renders plain text, not a slot |
| Rule between content and actions | `divider` |
| Rule under the full-screen app bar | `header-divider` (full-screen only) |
| An action in the full-screen app bar | `slot="header-action"` |
| Localized built-in buttons | `locale`, or `close-label` / `cancel-label` / `ok-label` |
| Open/close from code | `show()` / `close()`, or set `open` |

## API contract

```html
<md-dialog
  open                                 <!-- default: false; reflects -->
  headline="Discard draft?"            <!-- default: "" -->
  icon="delete"                        <!-- default: "" (Material Symbols name) -->
  fullscreen                           <!-- default: false -->
  scrim-dismissible="true|false"       <!-- default: true -->
  divider                              <!-- default: false -->
  header-divider                       <!-- default: false (full-screen only) -->
  close-label="Close"                  <!-- default: "" → derived from locale -->
  cancel-label="Cancel"                <!-- default: "" → derived from locale -->
  ok-label="OK"                        <!-- default: "" → derived from locale -->
  locale="en-US"                       <!-- default: en-US -->
  density="-1|-2|-3|-4"                <!-- default: 0 (uncompacted; only -1…-4 have rules) -->
>
  <p>Your changes will be lost.</p>
  <md-button slot="actions" variant="text">Cancel</md-button>
  <md-button slot="actions" variant="filled">Discard</md-button>
</md-dialog>
```

**Events** — `mdOpen`, `mdClose`, `mdCancel`, all `CustomEvent<void>` and all
default Stencil events (bubbling and composed).

**Methods** — `show(): Promise<void>` and `close(): Promise<void>`. Both just
set `open`, so `open` stays the single source of truth.

**Slots** — `(default)` body content · `actions` · `headline` and `icon`
(**basic variant only** — neither slot is rendered when `fullscreen`) ·
`header-action` (rendered only when `fullscreen`).

**Parts** — `scrim`, `container`, `header`, `icon`, `headline`, `close-button`,
`content`, `divider`, `actions`, `cancel-button`, `ok-button`.

### Behavioral contract worth knowing

- **You supply the action buttons** via `slot="actions"`. When that slot is
  empty the dialog renders two fallback buttons instead — a text Cancel (which
  emits `mdCancel` and closes) and a filled OK (which just closes). `cancel-label`
  / `ok-label` name only those fallbacks; any slotted button replaces both.
- **Slotted action buttons do not close the dialog.** They are your markup, so
  wire them to `close()` yourself.
- **Button order is yours to get right.** M3 puts dismissive actions on the
  leading side of confirming actions. The component does not reorder them.
- `mdCancel` fires only on dismissal — Escape, a scrim click, the full-screen
  close button, or the built-in fallback Cancel. `mdClose` fires on *every*
  close, including those. A dismissal therefore emits `mdCancel` **and**
  `mdClose`; don't handle the same case twice.
- `mdOpen` / `mdClose` fire from a watcher on `open`, so they do **not** fire on
  mount for a dialog that starts `open` — only on a subsequent change.
- **The role differs by variant**: the basic dialog's container is
  `role="alertdialog"`, the full-screen one is `role="dialog"`. Both are
  `aria-modal="true"`.
- **Full-screen dialogs have no scrim.** The scrim element (and therefore
  `scrim-dismissible`) exists only for the basic variant.
- **A full-screen dialog titles itself from the `headline` prop only.** Its app
  bar renders the prop as bare text; there is no `<slot name="headline">` and no
  `<slot name="icon">` outside the basic header. Passing
  `<md-dialog fullscreen><span slot="headline">…</span></md-dialog>` therefore
  renders an empty `part="headline"` element that `aria-labelledby` still points
  at — a full-screen dialog with no visible title **and no accessible name**.
  Use `headline="…"` there, and put rich markup in the body or in
  `slot="header-action"`.
- **Focus is handled for you.** On open the dialog focuses the first visible
  tabbable element — the trap descends into open shadow roots, so the `<input>`
  inside a slotted `md-text-field` counts — or the container itself if there is
  none. Tab and Shift+Tab wrap inside the dialog. On close, focus returns to
  whatever was focused before, with `preventScroll`.
- Escape closes the dialog and calls `preventDefault()` + `stopPropagation()`,
  so an outer Escape handler will not also fire.
- The dialog sets `document.body.style.overflow = 'hidden'` while open and
  clears it on close and on disconnect.
- The host is `display: contents`; the scrim and container are `position: fixed`
  at `z-index` 2147483646 / 2147483647, so the dialog paints above host-app
  chrome. Override with `--md-dialog-scrim-z-index` / `--md-dialog-z-index`.
- `icon` renders a Material Symbols ligature; it only shows a glyph if the
  Material Symbols font is loaded. Use `slot="icon"` for any other artwork.
- M3 wants confirming actions **disabled until a choice is made**; dismissive
  actions are never disabled. That is your logic, not the component's.

---

## Do / Don't

Sourced from [M3 · Dialogs · Guidelines](https://m3.material.io/components/dialogs/guidelines).

| ✅ Do | ❌ Don't |
|---|---|
| Use dialogs for prompts that block normal operation and need a decision or acknowledgement | Don't use dialogs for low/medium-priority information — use a snackbar |
| Pose a specific question, explain what's involved, give clear actions | Don't use an ambiguous headline |
| Shorten app-bar text and put long headlines in the content area of a full-screen dialog | Avoid long headlines in a full-screen dialog's app bar — truncation misleads |
| Disable confirming actions until a choice is made | Never disable dismissive actions |
| Place dismissive actions on the **leading** side of confirming actions | Don't put dismissive actions after the confirming one |
| Provide a single action only when it's an acknowledgement | Avoid unclear choices — "Cancel" makes no sense with no proposed action |
| Display two text buttons side by side | Stack them only for long labels; confirming above dismissive |
| Label the confirming action with the verb ("Create") | Don't use vague "OK" when a verb is clearer |
| Use a basic dialog to confirm discarding unsaved changes | Don't trigger a second dialog from a confirming action |
| Show field errors inline | Don't put field-level validation in a dialog |
| Keep the dialog self-contained | Beware actions that navigate away and leave it indeterminate |

---

## Patterns

```html
<!-- Destructive confirmation: no click-away, dismissive first -->
<md-button id="open-confirm">Discard draft</md-button>

<md-dialog id="confirm" headline="Discard draft?" icon="delete"
           scrim-dismissible="false">
  <p>Your changes will be lost.</p>
  <md-button slot="actions" variant="text" id="confirm-cancel">Cancel</md-button>
  <md-button slot="actions" variant="filled" id="confirm-discard">Discard</md-button>
</md-dialog>

<script type="module">
  const dlg = document.getElementById('confirm');

  document.getElementById('open-confirm')
    .addEventListener('mdClick', () => dlg.show());

  // Slotted buttons never close the dialog on their own.
  document.getElementById('confirm-cancel')
    .addEventListener('mdClick', () => dlg.close());

  document.getElementById('confirm-discard')
    .addEventListener('mdClick', () => {
      discardDraft();
      dlg.close();
    });

  // Dismissal only (Escape / close button). mdClose also fires here.
  dlg.addEventListener('mdCancel', () => console.log('dismissed'));

  function discardDraft() {}
</script>
```

```html
<!-- Full-screen sub-task: short app-bar headline, detail in the body -->
<md-dialog id="new-event" fullscreen headline="New event" header-divider>
  <md-button slot="header-action" variant="text" id="event-save">Save</md-button>

  <h2>Create a calendar event</h2>
  <md-text-field id="event-title" label="Title"></md-text-field>
</md-dialog>

<script type="module">
  const dlg = document.getElementById('new-event');
  const save = document.getElementById('event-save');

  // M3: keep the confirming action disabled until a choice is made.
  save.disabled = true;
  document.getElementById('event-title')
    .addEventListener('mdInput', (e) => { save.disabled = !e.detail; });

  save.addEventListener('mdClick', () => dlg.close());
</script>
```

```html
<!-- Acknowledgement only: one action -->
<md-dialog id="updated" headline="Update installed" icon="check_circle">
  <p>Version 2.4 is now active.</p>
  <md-button slot="actions" variant="text" id="updated-ok">Got it</md-button>
</md-dialog>

<script type="module">
  const dlg = document.getElementById('updated');
  document.getElementById('updated-ok')
    .addEventListener('mdClick', () => dlg.close());
</script>
```

```html
<!-- No slotted actions: the built-in Cancel / OK fallback, localized -->
<md-dialog id="simple" headline="Continuer ?" locale="fr-FR"></md-dialog>

<script type="module">
  const dlg = document.getElementById('simple');
  dlg.addEventListener('mdCancel', () => console.log('Annuler pressed'));
  dlg.addEventListener('mdClose', () => console.log('closed'));
  dlg.show();
</script>
```

## Anti-patterns

| ❌ Wrong | ✅ Right | Why |
|---|---|---|
| Expecting a slotted action button to close the dialog | Call `close()` in its handler | Only the built-in fallback buttons close by themselves. |
| Setting `ok-label` while also slotting `[slot="actions"]` | Pick one source of actions | The slot replaces the fallback buttons, so the label is dead config. |
| Handling `mdCancel` and `mdClose` as mutually exclusive | `mdCancel` implies `mdClose` | A dismissal emits both — you'll double-handle. |
| Waiting for `mdOpen` on a dialog rendered with `open` | Assume it is open, or call `show()` after mount | The event comes from the `open` watcher and does not fire on mount. |
| Dismissive action after the confirming one | Dismissive on the leading side | M3 explicit rule; the component won't reorder. |
| `scrim-dismissible` left on for a destructive confirm | `scrim-dismissible="false"` | Accidental click-away destroys data. |
| `scrim-dismissible="false"` on a `fullscreen` dialog | Drop it | Full-screen dialogs render no scrim, so the prop is inert. |
| A disabled Cancel button | Never disable dismissive actions | M3 explicit rule. |
| Writing your own focus trap or restore-focus code around it | Let the dialog do it | It traps Tab, focuses the first tabbable on open and restores focus on close. |
| Setting `role` or `aria-modal` on `<md-dialog>` | Leave them alone | The container inside the shadow root already carries them. |
| A dialog for "Saved successfully" | `md-snackbar` | Low priority, non-blocking. |
| Opening a second dialog from the confirming action | Resolve in one | M3 explicit rule. |
| A long headline in a `fullscreen` dialog's bar | Short bar text, detail in the body | Truncation misleads. |
| `slot="headline"` or `slot="icon"` on a `fullscreen` dialog | `headline="…"`; drop the icon | Both slots live inside the basic header only. The slotted node never renders, and the empty headline element still gets `aria-labelledby` — the dialog loses its accessible name. |
| Field validation errors in a dialog | Inline on the field | M3 explicit rule. |

## Accessibility, RTL, density, i18n

**Accessibility**
- The dialog is modal: `aria-modal="true"`, focus is trapped while open, and
  Escape dismisses (firing `mdCancel` then `mdClose`). Focus restoration on
  close is automatic.
- The container is `role="alertdialog"` for the basic variant and
  `role="dialog"` when `fullscreen`.
- `headline` (the prop, or the `headline` slot in the basic variant) is wired to
  `aria-labelledby`; the content region is wired to `aria-describedby`. If you
  slot a headline, keep real text in it or the dialog loses its accessible name
  — and note the slot is not rendered when `fullscreen`, so a full-screen dialog
  must use the `headline` prop. An `aria-label` on the `<md-dialog>` host is not
  a fallback: `role="dialog"` sits on the container inside the shadow root, not
  on the host.
- The full-screen close button gets its accessible name from `close-label`, or
  from `locale` when that is empty.
- The focus trap only sees **open** shadow roots. A third-party component with a
  closed shadow root inside the dialog contributes no tab stop.
- Keep DOM order of the actions meaningful — that is the order screen readers
  and keyboard users get.

**RTL** — the container, header and actions row use logical properties and
mirror under `dir="rtl"`. "Dismissive first" means **leading**, so it renders on
the right in RTL automatically; do not hardcode a side.

**Density** — set `density="-1"` … `density="-4"` for a local rung, or inherit a
global `data-density` ancestor. Rung `0` is the uncompacted default and has no
rule of its own, so `density="0"` does **not** opt a dialog out of an inherited
rung; use `style="--md-sys-density-scale: 0"` to reset the scale locally.
Density compacts corner radius, header/content padding, headline and body type,
and the full-screen app-bar height.

**i18n** — translate `headline`, body content and your slotted action labels.
`locale` picks the built-in Close / Cancel / OK strings; `ar`, `de`, `es`, `fr`,
`hi`, `it`, `ja`, `ko`, `nl`, `pl`, `pt`, `ro`, `ru` and `zh` are built in and
anything else falls back to English. `close-label` / `cancel-label` / `ok-label`
override the resolved string. Translated action labels run longer — check
whether buttons need stacking.

## Related components

`md-snackbar` · `md-bottom-sheet` · `md-side-sheet` · `md-menu` ·
`md-card` · `md-button` · `md-tooltip`

## Theming

| Custom property | Purpose | Default |
|---|---|---|
| `--md-dialog-container-color` | Surface background | `--md-sys-color-surface-container-high` |
| `--md-dialog-container-shape` | Corner radius | `max(16px, 28px + density × 2px)` |
| `--md-dialog-headline-color` | Headline text colour | `--md-sys-color-on-surface` |
| `--md-dialog-content-color` | Supporting-text colour | `--md-sys-color-on-surface-variant` |
| `--md-dialog-icon-color` | Header glyph colour | `--md-sys-color-secondary` |
| `--md-dialog-icon-size` | Header glyph box | `24px` |
| `--md-dialog-scrim-color` | Backdrop colour (basic only) | `rgba(0, 0, 0, 0.32)` |
| `--md-dialog-divider-color` | Divider rules | `--md-sys-color-outline-variant` |
| `--md-dialog-z-index` | Surface stacking | `2147483647` |
| `--md-dialog-scrim-z-index` | Scrim stacking | `2147483646` |

**CSS parts** — `scrim`, `container`, `header`, `icon`, `headline`,
`close-button`, `content`, `divider`, `actions`, `cancel-button`, `ok-button`.

```css
md-dialog.branded {
  --md-dialog-container-color: var(--md-sys-color-surface-container-highest);
  --md-dialog-scrim-color: rgba(0, 0, 0, 0.6);
}

md-dialog.branded::part(headline) {
  text-align: center;
}
```

<!-- Auto Generated Below -->


## Properties

| Property           | Attribute           | Description                                                                                                                                                                                           | Type                        | Default   |
| ------------------ | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- | --------- |
| `cancelLabel`      | `cancel-label`      | Label for the default cancel action button (slot fallback). Leave empty to derive from `locale`.                                                                                                      | `string`                    | `''`      |
| `closeLabel`       | `close-label`       | Accessible label for the full-screen close button. Leave empty to derive from `locale`.                                                                                                               | `string`                    | `''`      |
| `density`          | `density`           | Local density rung. Drives the same `--md-sys-density-scale` signal that a global `data-density` ancestor sets, so a local value simply overrides the inherited one. 0 = default, -4 = ultra-compact. | `-1 \| -2 \| -3 \| -4 \| 0` | `0`       |
| `divider`          | `divider`           | Show divider between content and actions                                                                                                                                                              | `boolean`                   | `false`   |
| `fullscreen`       | `fullscreen`        | Full-screen variant                                                                                                                                                                                   | `boolean`                   | `false`   |
| `headerDivider`    | `header-divider`    | Show divider between header and content (full-screen)                                                                                                                                                 | `boolean`                   | `false`   |
| `headline`         | `headline`          | Headline text (or use the headline slot)                                                                                                                                                              | `string`                    | `''`      |
| `icon`             | `icon`              | Icon name (Material Symbols shorthand, or use the icon slot)                                                                                                                                          | `string`                    | `''`      |
| `locale`           | `locale`            | BCP-47 locale for built-in button labels (Close, Cancel, OK).                                                                                                                                         | `string`                    | `'en-US'` |
| `okLabel`          | `ok-label`          | Label for the default confirm action button (slot fallback). Leave empty to derive from `locale`.                                                                                                     | `string`                    | `''`      |
| `open`             | `open`              | Whether the dialog is open                                                                                                                                                                            | `boolean`                   | `false`   |
| `scrimDismissible` | `scrim-dismissible` | Whether clicking the scrim closes the dialog                                                                                                                                                          | `boolean`                   | `true`    |


## Events

| Event      | Description                              | Type                |
| ---------- | ---------------------------------------- | ------------------- |
| `mdCancel` | Emits when dismissed via scrim or Escape | `CustomEvent<void>` |
| `mdClose`  | Emits when the dialog closes             | `CustomEvent<void>` |
| `mdOpen`   | Emits when the dialog opens              | `CustomEvent<void>` |


## Methods

### `close() => Promise<void>`

Close the dialog

#### Returns

Type: `Promise<void>`



### `show() => Promise<void>`

Open the dialog

#### Returns

Type: `Promise<void>`




## Shadow Parts

| Part              | Description |
| ----------------- | ----------- |
| `"actions"`       |             |
| `"cancel-button"` |             |
| `"close-button"`  |             |
| `"container"`     |             |
| `"content"`       |             |
| `"divider"`       |             |
| `"header"`        |             |
| `"headline"`      |             |
| `"icon"`          |             |
| `"ok-button"`     |             |
| `"scrim"`         |             |


## Dependencies

### Depends on

- [md-button](../md-button)

### Graph
```mermaid
graph TD;
  md-dialog --> md-button
  md-button --> md-ripple
  md-button --> md-loading-indicator
  style md-dialog fill:#f9f,stroke:#333,stroke-width:4px
```

----------------------------------------------

*Built with [StencilJS](https://stenciljs.com/)*

AWC UI Operator’s Manual

# main-llm.md — AWC UI build director

<!-- llm:meta
role: director
audience: llm
library: "@awc-ui/core"
component-count: 57
sub-component-count: 24
manual-count: 81
per-component-docs: ./packages/core/src/components/<tag>/readme.md
-->

**You are building a web app with AWC UI, a Material Design 3 web-component
library.** This file is the entry point and the only document you need before
writing UI code. Everything here is self-contained: tokens, recipes,
composition rules and the ship checklist. Per-component detail lives in
`./packages/core/src/components/<tag>/readme.md` — e.g.
[`md-button`](./packages/core/src/components/md-button/readme.md).

Your job, in order:

1. **Interview** the user (§1). Do not skip it and do not guess.
2. **Lock the configuration** their answers imply (§2) and bootstrap it (§3–§4).
3. **Route every UI need through the decision matrix** (§5) — never pick a
   component by name-similarity.
4. **Load the component's readme.md before writing a single line of its
   markup** (§6). The file is
   `./packages/core/src/components/<tag>/readme.md` — read it in full, do not
   skim it, and do not write the markup from memory of a similar library. Each
   one has a `When NOT to use`, a `Do / Don't` table sourced from
   [m3.material.io](https://m3.material.io), and an `Anti-patterns` table of
   mistakes models actually make. If you are about to use three components,
   load all three readmes first.
5. **Check what nests inside what** (§7) and start from a recipe (§8) rather
   than from a blank page.
6. **Apply the universal rules** (§9) — the API, content and accessibility
   rules every component is bound by, and the ones §10 checks you against.
7. **Run the ship checklist** (§10) before declaring done.

**Fail closed.** If you cannot satisfy a step — no component fits, a token
doesn't exist, an accessible name has nowhere to come from — say so and ask.
Do not invent an `md-*` tag, a prop, or a token. If it is not in this file or
in the component's manual, it does not exist.

---

## §1 — Interview the user

Ask these **one at a time**, in this order. Each answer closes off decisions
downstream, so don't batch them into a wall of questions. Skip a question only
if the user has already answered it unprompted.

If the user says "just pick sensible defaults", use the **bold** option and tell
them what you chose.

### 1.1 Scope and shape

1. **What is the app?** One or two sentences — domain, primary job, who uses it.
2. **What kind of surface is it?**
   - Internal tool / admin console / dashboard
   - Data-heavy CRUD application
   - Consumer-facing product
   - Marketing or content site
   - Mobile-first / PWA
3. **Which framework?** React · Angular · Vue · Svelte · **plain HTML** ·
   Next · Nuxt · SvelteKit · Astro
4. **Does it server-render?** (SSR/SSG, or **client-only SPA**)
5. **Roughly how many distinct screens**, and what are the top 3?

### 1.2 Look and feel

6. **Density** — how much information per screen?
   - `0` — **default**, comfortable, touch-friendly
   - `-1` / `-2` — compact; typical for admin consoles
   - `-3` / `-4` — ultra-compact; dense data tables, trading/ops screens
7. **Theme** — light only, dark only, or **both with a user toggle**?
   Does it follow the OS preference?
8. **Brand color** — a seed/primary color, or **stock MD3 palette**?
9. **Expressive motion** — keep **ripple and shape-morph on** (default), or turn
   them off for a flatter, more utilitarian feel?
10. **Shape language** — **rounded** (MD3 default) or squared?

### 1.3 Internationalization

11. **How many locales**, and which?
12. **Any RTL locales** (Arabic, Hebrew, Farsi, Urdu)? — this changes layout
    verification and directional-icon handling.
13. **Which i18n engine?** (i18next, vue-i18n, ngx-translate, Paraglide, custom)
    Components are engine-agnostic — you localize in the consumer layer.
14. **Locale-formatted values** — dates, numbers, currency? Which locale drives
    `Intl`?

### 1.4 Data and forms

15. **Is there significant tabular data?** How many rows, and is it
    server-paged? (Drives how you page `md-table` — it holds the state, you
    supply each page of rows.)
16. **Are there charts?** Which questions should they answer?
17. **How heavy are the forms?** Validation rules, async validation, multi-step?
18. **Rich text editing anywhere?** — ⚠️ **AWC UI has no RTE component.** If yes,
    you must integrate a third-party editor (TipTap, Lexical, Quill) and style
    it to the MD3 tokens yourself. Confirm this with the user explicitly.

### 1.5 Constraints

19. **Accessibility target** — **WCAG 2.1 AA** (what the library is tested to),
    or stricter?
20. **Browser/device support floor?**
21. **Anything already decided** you must not change — existing design system,
    router, state library, CSS approach?

---

## §2 — Map answers to configuration

| Answer | What you set |
|---|---|
| Admin console / data-heavy | `data-density="-1"` or `-2` on `<html>`; prefer `size="xs"`/`"sm"` on actions |
| Consumer / marketing | leave density alone (`0` is the default); larger button sizes (`md`/`lg`) for CTAs |
| Mobile-first | `md-navigation-bar` + `md-fab`; avoid `md-navigation-rail`, `md-transfer-list`, wide tables |
| Desktop-first | `md-navigation-rail` or `md-app-bar`; rail over bottom bar |
| Dark mode | `data-theme="dark"` on `<html>`; wire a toggle, and mirror OS via `prefers-color-scheme` |
| Both themes with toggle | persist the choice; set the attribute before first paint to avoid a flash |
| Brand color | override the `--md-sys-color-*` roles in your own stylesheet, loaded after the tokens (§4.2) |
| Flat / utilitarian | `data-ripple="off"` and `data-shape-morph="off"` on `<html>` |
| Any RTL locale | `dir="rtl"` on `<html>`; add `mirror-icon` to `md-button`s with directional glyphs; swap the glyph name yourself everywhere else (§4.5) |
| Multiple locales | build a dictionary in the consumer layer and feed component text props from it; never hardcode strings in markup |
| `Intl`-formatted values | pass a `locale` prop where a component exposes one; format everything else before it reaches the component |
| SSR | import from `@awc-ui/core/hydrate` on the server; use the client/server wrappers in `@awc-ui/react` |
| Heavy forms | components are form-associated via `ElementInternals` — use a real `<form>`, `md-button type="submit"`, and native `required` (§4.7) |
| Rich text | integrate a third-party editor; there is no `md-rich-text` |

### 2.1 Global switches — the complete set

All are attributes on `<html>` (or any ancestor; the nearest one wins).

```html
<html
  lang="en"
  dir="ltr"                  <!-- or rtl -->
  data-theme="dark"          <!-- omit for light -->
  data-density="-1"          <!-- -1 … -4; see below -->
  data-ripple="off"          <!-- default on -->
  data-shape-morph="off"     <!-- default on -->
>
```

**`data-density="0"` is inert.** No `[density="0"]` rule exists — density `0` is
simply the base values on `:root`, and a `0` rule would pin them onto every
element (reflected props write `density="0"` almost everywhere) and break global
inheritance. So the *overriding* range is `-1 … -4`. To escape an inherited rung
for one subtree, reset the scale directly:

```css
.opt-out-of-density { --md-sys-density-scale: 0; }
```

Per-component overrides beat the global one: a `density` prop, or
`ripple="off"` / `shape-morph="off"` on the element. **53 of the 57 top-level
components expose a `density` prop.** The four that don't: `md-divider`,
`md-ripple`, `md-sparkline`, `md-tabs`. (Nine sub-components also lack one —
they inherit density from the parent that owns their layout.)

---

## §3 — Install and bootstrap

```bash
npm install @awc-ui/core
```

**Register everything (recommended).** Two imports, once per app entry — one
defines every component, one loads the tokens. Both come from `@awc-ui/core`
itself, so this path needs no second package:

```ts
import { defineCustomElements } from '@awc-ui/core/loader';
import '@awc-ui/core/css/tokens.css';

defineCustomElements(window);
```

`@awc-ui/core/css/tokens.css` is the complete, self-contained token sheet —
light and dark colour roles, shape, elevation, motion, typescale, spacing and
z-index. It is the same set documented in §4.1.

**One-line alternative.** `@awc-ui/core/define` does both steps in a single
import. It loads the package's own token sheet, so it needs nothing else
installed:

```ts
import '@awc-ui/core/define'; // defines every component + loads the token sheet
```

`define` is client-only (it eval-guards `window`) and, because it imports CSS,
it needs a bundler. Never import it into a server graph.

**Per-component registration**, for size-sensitive bundles — load the token
sheet once yourself:

```ts
import '@awc-ui/core/css/tokens.css';
import '@awc-ui/core/components/md-button';
import '@awc-ui/core/components/md-text-field';
```

**Framework wrappers** — use these instead of raw elements; they handle
registration, typed props, and event binding. Below the version floor, drop to
the raw custom elements and the `loader` import above.

| Framework | Package | Requires |
|---|---|---|
| React / Next | `@awc-ui/react` | React 18+ |
| Angular | `@awc-ui/angular` (`AwcUiModule`) | Angular 17+ |
| Vue / Nuxt | `@awc-ui/vue` | Vue 3 |
| Svelte / SvelteKit | `@awc-ui/svelte` | Svelte 4+ |
| Plain HTML / Astro | `@awc-ui/core/loader` | — |

**SSR** — `@awc-ui/core/hydrate` renders Declarative Shadow DOM on the server.
`@awc-ui/react` ships matching client/server wrappers. Keep `define` and
`loader` out of the server graph; both are browser entries.

**Fonts** — components expect Roboto and Material Symbols Outlined to be
available. The library does **not** inject them. Every `icon="…"` prop renders a
Material Symbols glyph inside shadow DOM, so the font must be registered at the
**document** level (font registration crosses shadow boundaries; class rules do
not — the components declare the class rule inside their own roots):

```html
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap">
```

To swap the whole library to a different Material Symbols cut, set
`--md-sys-icon-font-family` on `:root` and load the matching `@font-face`.

---

## §4 — Global configuration reference

### 4.1 The token system

All AWC UI styling resolves to design tokens. **Never hardcode hex colours,
pixel radii, shadows, or font stacks** — they break theming, dark mode, density
and RTL in one stroke. Every value below is a real custom property defined by
`@awc-ui/tokens`; you may read them, and you may override them.

#### Color roles

Light is the default; `[data-theme="dark"]` swaps the palette. Every role has a
matching `on-` role for content drawn on top of it.

| Token | Light | Dark | Typical usage |
|---|---|---|---|
| `--md-sys-color-primary` | `#6750A4` | `#D0BCFF` | Filled buttons, active states |
| `--md-sys-color-on-primary` | `#FFFFFF` | `#381E72` | Text/icons on primary |
| `--md-sys-color-primary-container` | `#EADDFF` | `#4F378B` | FAB, selected segments |
| `--md-sys-color-on-primary-container` | `#21005D` | `#EADDFF` | Text on primary-container |
| `--md-sys-color-secondary` | `#625B71` | `#CCC2DC` | Secondary accents |
| `--md-sys-color-secondary-container` | `#E8DEF8` | `#4A4458` | Tonal buttons, chips |
| `--md-sys-color-tertiary` | `#7D5260` | `#EFB8C8` | Tertiary accents |
| `--md-sys-color-tertiary-container` | `#FFD8E4` | `#633B48` | Tertiary surfaces |
| `--md-sys-color-error` | `#B3261E` | `#F2B8B5` | Error / destructive |
| `--md-sys-color-on-error` | `#FFFFFF` | `#601410` | Content on error |
| `--md-sys-color-error-container` | `#F9DEDC` | `#8C1D18` | Error surfaces |
| `--md-sys-color-surface` | `#FFFBFE` | `#1C1B1F` | Page / card background |
| `--md-sys-color-on-surface` | `#1C1B1F` | `#E6E1E5` | Body text |
| `--md-sys-color-surface-variant` | `#E7E0EC` | `#49454F` | Muted fills |
| `--md-sys-color-on-surface-variant` | `#49454F` | `#CAC4D0` | Secondary text, icons |
| `--md-sys-color-surface-container-lowest` | `#FFFFFF` | `#0F0D13` | Lowest surface tier |
| `--md-sys-color-surface-container-low` | `#F7F2FA` | `#1D1B20` | Card / elevated button bg |
| `--md-sys-color-surface-container` | `#F3EDF7` | `#211F26` | Default container tier |
| `--md-sys-color-surface-container-high` | `#ECE6F0` | `#2B2930` | Dialog / menu surfaces |
| `--md-sys-color-surface-container-highest` | `#E6E0E9` | `#36343B` | Highest surface tier |
| `--md-sys-color-outline` | `#79747E` | `#938F99` | Borders (3:1 contrast) |
| `--md-sys-color-outline-variant` | `#CAC4D0` | `#49454F` | Dividers (low contrast) |
| `--md-sys-color-inverse-surface` | `#313033` | `#E6E1E5` | Snackbar, inverse chips |
| `--md-sys-color-inverse-on-surface` | `#F4EFF4` | `#313033` | Content on inverse-surface |
| `--md-sys-color-scrim` | `#000000` | `#000000` | Modal scrims |

**Semantic status roles** — beyond the baseline M3 palette, the library adds
`success`, `warning` and `info`, each with `on-`, `-container` and
`on-…-container` companions, in both themes, AA-verified. Used by `md-chip`,
`md-badge`, `md-meter`, `md-status-dot`.

| Token | Light | Dark |
|---|---|---|
| `--md-sys-color-success` | `#2E6B4F` | `#9DD5B0` |
| `--md-sys-color-success-container` | `#B8F0CE` | `#14512F` |
| `--md-sys-color-warning` | `#7A5900` | `#EFC148` |
| `--md-sys-color-warning-container` | `#FFDF9B` | `#5C4200` |
| `--md-sys-color-info` | `#38608F` | `#A2C9FE` |
| `--md-sys-color-info-container` | `#D3E4FF` | `#1E4975` |

#### Shape tokens

| Token | Value | Typical usage |
|---|---|---|
| `--md-sys-shape-corner-none` | `0px` | Square edges |
| `--md-sys-shape-corner-extra-small` | `4px` | Snackbar, small chips |
| `--md-sys-shape-corner-small` | `8px` | Cards (outlined), small surfaces |
| `--md-sys-shape-corner-medium` | `12px` | Cards (default), inputs |
| `--md-sys-shape-corner-large` | `16px` | Dialogs, large cards |
| `--md-sys-shape-corner-extra-large` | `28px` | Bottom sheets |
| `--md-sys-shape-corner-full` | `9999px` | Buttons, chips, FAB |

Partial-corner variants also exist for surfaces that meet an edge:
`--md-sys-shape-corner-extra-small-top`, `-large-top`, `-large-end`,
`-extra-large-top`. Their values are four-value `border-radius` shorthands
(e.g. `16px 16px 0px 0px`), so assign them to `border-radius`, not to a single
corner.

#### Elevation tokens

`-0` is the keyword `none`; `-1` through `-5` are complete `box-shadow` values,
and those five are **re-declared with deeper shadows under `[data-theme="dark"]`**
— so always use the token rather than copying its value.

| Token | When |
|---|---|
| `--md-sys-elevation-0` | `none` — resting cards, flat surfaces |
| `--md-sys-elevation-1` | Elevated buttons and cards at rest |
| `--md-sys-elevation-2` | Hover bump |
| `--md-sys-elevation-3` | FAB, menus, dialogs at rest |
| `--md-sys-elevation-4` – `-5` | Reserved for the most prominent surfaces |

#### Motion tokens

Durations come in four families of four: `short1…4` (50/100/150/200ms),
`medium1…4` (250/300/350/400ms), `long1…4` (450/500/550/600ms), and
`extra-long1…4` (700/800/900/1000ms).

| Token | Value | When |
|---|---|---|
| `--md-sys-motion-duration-short2` | `100ms` | Buttons, ripples, state layers |
| `--md-sys-motion-duration-medium2` | `300ms` | Dialogs, sheets, menus |
| `--md-sys-motion-duration-long2` | `500ms` | Large surface transitions |
| `--md-sys-motion-duration-extra-long1` | `700ms` | Ambient / chart entrances |
| `--md-sys-motion-easing-standard` | `cubic-bezier(0.2, 0, 0, 1)` | Small utility transitions |
| `--md-sys-motion-easing-standard-accelerate` | `cubic-bezier(0.3, 0, 1, 1)` | Exits |
| `--md-sys-motion-easing-standard-decelerate` | `cubic-bezier(0, 0, 0, 1)` | Entrances |
| `--md-sys-motion-easing-emphasized` | `cubic-bezier(0.2, 0, 0, 1)` | Expressive transitions |
| `--md-sys-motion-easing-emphasized-accelerate` | `cubic-bezier(0.3, 0, 0.8, 0.15)` | Expressive exits |
| `--md-sys-motion-easing-emphasized-decelerate` | `cubic-bezier(0.05, 0.7, 0.1, 1)` | Expressive entrances |
| `--md-sys-motion-easing-linear` | `linear` | Progress, indeterminate loops |

> `--md-sys-motion-easing-emphasized` deliberately equals the standard curve:
> the full M3 emphasized curve is a two-phase path interpolator that CSS cannot
> express. The accelerate and decelerate halves *are* expressible, and are the
> ones to reach for.

MD3 Expressive also ships spring pairs, each an easing plus its natural
duration: `--md-sys-motion-spring-{spatial,effects}-{fast,default,slow}-easing`
and `…-duration`. Spatial = position/size/shape; effects = colour/opacity.

#### State-layer opacities

| Token | Value |
|---|---|
| `--md-sys-state-hover-state-layer-opacity` | `0.08` |
| `--md-sys-state-focus-state-layer-opacity` | `0.12` |
| `--md-sys-state-pressed-state-layer-opacity` | `0.12` |
| `--md-sys-state-dragged-state-layer-opacity` | `0.16` |
| `--md-sys-state-disabled-container-opacity` | `0.12` |
| `--md-sys-state-disabled-content-opacity` | `0.38` |

#### Typography, spacing and layering

- **Typescale** — `--md-sys-typescale-<role>-<size>-*` for
  `display`/`headline`/`title`/`label`/`body` × `large`/`medium`/`small`. Each
  role exposes `-font-family`, `-font-size`, `-line-height`, `-font-weight`,
  `-letter-spacing`, plus a `-font` shorthand
  (e.g. `--md-sys-typescale-headline-medium-font: 400 28px/36px Roboto, sans-serif`).
  Use the shorthand on the `font` property for headings you write yourself.
- **Spacing** — a 4px scale that tightens with density:
  `--md-sys-spacing-inset-{xs,sm,md,lg,xl}` (internal padding, 4/8/12/16/24px)
  and `--md-sys-spacing-gap-{xs,sm,md,lg}` (between siblings, 4/8/12/16px), plus
  `--md-sys-spacing-row-height` (`56px` at density 0).
- **Layering** — `--md-sys-z-index-app-bar` (100), `-navigation` (200),
  `-bottom-sheet` (300), `-popup` (1000), `-dialog-scrim` (1001), `-tooltip`
  (1500), `-snackbar` (2000). Use these instead of inventing z-indexes, or your
  overlay will land under a menu.

### 4.2 Theming and rebranding

To rebrand, redefine the role variables **after** the tokens stylesheet — both
themes, or dark mode inherits your light brand colour:

```css
:root {
  --md-sys-color-primary: #00629E;
  --md-sys-color-on-primary: #FFFFFF;
  --md-sys-color-primary-container: #CFE5FF;
  --md-sys-color-on-primary-container: #001D33;
}
[data-theme="dark"] {
  --md-sys-color-primary: #9BCBFF;
  --md-sys-color-on-primary: #003354;
  --md-sys-color-primary-container: #004A78;
  --md-sys-color-on-primary-container: #CFE5FF;
}
```

Per-component knobs are `--md-<component>-*` custom properties, listed in each
manual's Theming section (`--md-button-container-color`,
`--md-card-container-shape`, …). Prefer those over `::part()`, and prefer
`::part()` over reaching into shadow internals, which are unstable and will
break on any release.

**Overriding a colour role forfeits the library's contrast testing.** Re-verify
AA (4.5:1 text, 3:1 borders and dividers) after any palette change. The Theme
Generator at <https://awc-ui.dev/theme-generator> takes a seed colour, emits both
palettes as `--md-sys-color-*` overrides, and runs the WCAG checks live — use it
rather than hand-picking container and `on-` pairs.

### 4.3 Density

`data-density` steps `-1 → -4`; each rung trims ~4px of padding and touch
target, driving `--md-sys-density-scale` and the spacing tokens. `-4` is the
floor. Set it globally, override locally with the `density` prop. Do not go
below `-2` on touch-primary surfaces — you will break the 48px target.

Two details that bite:

- **`density="0"` does nothing** — see §2 for why, and for the
  `--md-sys-density-scale: 0` escape hatch.
- **`md-table` accepts two vocabularies**: the semantic `compact` / `standard` /
  `comfortable` (row heights 36 / 52 / 60px) *and* the numeric rungs. They
  compose — `density="compact"` inside a `data-density="-2"` region condenses
  further.

### 4.4 Dark mode

Set `data-theme="dark"` on `<html>` (or any ancestor — the nearest wins). The
tokens swap automatically and inherit across every shadow boundary; no
per-component code. To mirror the OS, read `prefers-color-scheme` and write the
attribute **before first paint**, or the page flashes light.

### 4.5 RTL

Layout is written with CSS logical properties, so it flips with `dir="rtl"`
without extra work. Two things still need you:

- **Directional glyphs do not flip.** Material Symbols are not auto-mirrored.
  `md-button` has a `mirror-icon` prop that mirrors its own leading/trailing
  glyph — set it on buttons using arrows, chevrons, `send`, `reply`, and leave
  it off for `add`, `search`, `favorite`. **`mirror-icon` exists only on
  `md-button`.** Everywhere else — `md-icon-button icon="…"`, `md-list-item`
  leading/trailing icons, `md-app-bar leading-icon` — swap the glyph name
  yourself (`arrow_back` ↔ `arrow_forward`).
- **Icon props you supply yourself** — e.g. `md-transfer-list`'s
  `move-right-icon` / `move-left-icon` / `move-all-*-icon` — must be swapped by
  you when the direction flips.

Never write physical CSS (`margin-left`, `padding-right`, `left`) around these
components. Use `margin-inline-start`, `padding-inline-end`, `inset-inline-end`.

### 4.6 Internationalization

Components are **i18n-engine-agnostic by design**. Every user-visible string is
either slotted content or a prop. Localize in the consumer layer: resolve your
dictionary to plain strings, then pass them in. Do not add a translation engine
inside a component. `locale` props exist only where a component computes an
`Intl`-formatted value itself: `md-date-picker`, `md-number-field`,
`md-meter`, `md-dialog`, and the charts (`md-bar-chart`, `md-line-chart`,
`md-area-chart`, `md-pie-chart`). `md-time-picker` has no
`locale` prop — it takes `format="12h" | "24h"` instead.

Templated strings keep their placeholder tokens when translated — translate
around the braces, don't remove them:

```html
<md-transfer-list count-template="{checked} / {total} ausgewählt"></md-transfer-list>
```

Other templated props: `md-autocomplete status-template`, `md-otp-field
cell-label-template`. Default prop values are English (`Search`, `Dismiss`,
`No results`, `Move selected to target`, …) — every one of them needs
translating in a localized app.

### 4.7 Forms and validation

Fourteen components are form-associated via `ElementInternals`, so they
participate in `FormData` and constraint validation like native controls:

`md-text-field` · `md-number-field` · `md-otp-field` · `md-select` ·
`md-multi-select` · `md-autocomplete` · `md-checkbox` · `md-radio` ·
`md-switch` · `md-slider` · `md-rating` · `md-date-picker` · `md-time-picker` ·
`md-button`

Rules:

- Use a real `<form>`. `md-button type="submit"` calls `form.requestSubmit()`
  (not `submit()`), so the `submit` event fires **and** built-in constraint
  validation runs. `required` genuinely blocks submit.
  `md-button type="reset"` calls `form.reset()`.
- Give every control a `name`, or it will not appear in `FormData`.
- Do **not** add hidden `<input>`s to mirror values — that's the old pattern and
  it double-submits.
- Validity changes are announced on a `mdValidityChange` event on the control.
  Error presentation is `error` + `error-text` on the field.
- Boolean state props differ by control — `md-checkbox` uses `checked`,
  `md-switch` uses **`selected`**, `md-select-option` uses `selected`. Check the
  manual; guessing `checked` on a switch silently does nothing.

---

## §5 — Component decision matrix

Route by **need**, not by name. If the need isn't listed, find the closest row
and read that component's `When NOT to use`.

### 5.1 Actions

| Need | Use | Don't use |
|---|---|---|
| Discrete labelled action | `md-button` | `md-chip`, raw `<button>` |
| Icon-only action | `md-icon-button` | `md-button` with no label |
| A destructive action | `md-button` + `md-dialog` to confirm | an unconfirmed `filled` button |
| The single most prominent screen action (mobile) | `md-fab` | a second `filled` `md-button` |
| One prominent action that expands to several | `md-fab-menu` + `md-fab-menu-item` | a stack of FABs |
| Primary action + variants of it | `md-split-button` | button + separate menu |
| 2–5 related actions as one unit | `md-button-group` | loose adjacent buttons |
| Mutually exclusive view/mode switch | `md-segmented-button-set` + `md-segmented-button` | radio buttons, tabs |
| Overflow / contextual actions | `md-menu` + `md-menu-item` | a row of text buttons |

### 5.2 Text input

| Need | Use | Don't use |
|---|---|---|
| Any single-line or multi-line text entry | `md-text-field` | raw `<input>` |
| A number with steppers and locale formatting | `md-number-field` | `md-text-field type="number"` |
| A one-time code / PIN | `md-otp-field` | a row of text fields |
| Text entry with suggestions | `md-autocomplete` | `md-select` |
| Site/app-wide search with a results surface | `md-search` | `md-text-field` with an icon |

### 5.3 Selection

| Need | Use | Don't use |
|---|---|---|
| One of many, from a list | `md-select` + `md-select-option` | radio group over ~7 options |
| One of few (2–5), all visible | `md-radio` | `md-select` |
| Several of many | `md-multi-select` | many `md-checkbox`es |
| Several of few, all visible | `md-checkbox` | `md-multi-select` |
| Instant on/off setting | `md-switch` | `md-checkbox` |
| A value in a numeric range | `md-slider` | `md-text-field type=number` |
| Subjective score | `md-rating` | slider |
| A color | `md-color-picker` | `<input type=color>` |
| Assign a subset from a bounded pool, side by side | `md-transfer-list` | two lists + buttons |
| Filter / attribute / removable entry | `md-chip` | small buttons |
| A date | `md-date-picker` | three selects |
| A time | `md-time-picker` | text field |

### 5.4 Navigation

| Need | Use | Don't use |
|---|---|---|
| Top-level destinations, mobile | `md-navigation-bar` + `md-navigation-tab` | tabs |
| Top-level destinations, desktop | `md-navigation-rail` + `md-navigation-rail-tab` | bottom bar |
| App header: title, actions, search | `md-app-bar` | a custom `<header>` |
| A dense action strip | `md-toolbar` | app bar |
| Sibling views **within** one screen | `md-tabs` + `md-tab` + `md-tab-panels` + `md-tab-panel` | navigation bar |
| Hierarchy / where-am-I | `md-breadcrumbs` + `md-breadcrumb-item` | text links |
| A linear multi-step flow | `md-stepper` + `md-step` | tabs |
| Contextual popup actions | `md-menu` + `md-menu-item`, `md-menu-item-group`, `md-sub-menu-item` | dialog |
| Hierarchical command menu (File → Export → PDF) | `md-menu` + `md-sub-menu-item` | nested dialogs |

### 5.5 Containment and feedback

| Need | Use | Don't use |
|---|---|---|
| Group related content | `md-card` | a bare `<div>` with a border |
| Blocking decision or focused task | `md-dialog` | a new page |
| Critical error the user must acknowledge | `md-dialog` | `md-snackbar` |
| Supplementary content from the bottom (mobile) | `md-bottom-sheet` | dialog |
| Supplementary content from the side (desktop) | `md-side-sheet` | dialog |
| Brief confirmation of an action, optionally undoable | `md-snackbar` | dialog, alert |
| Explain a control on hover/focus | `md-tooltip` | a dialog or inline hint |
| Progressive disclosure of sections | `md-accordion` + `md-accordion-item` | tabs |
| Visual separation | `md-divider` | a styled `<hr>` |
| A vertical set of records | `md-list` + `md-list-item` | a table |
| Determinate/indeterminate progress | `md-progress-indicator` | spinner GIF |
| Brand-consistent page/content loading | `md-loading-indicator` | custom spinner |
| Content-shaped loading placeholder | `md-skeleton` | a spinner over the whole page |
| Count or status on an element | `md-badge` | superscript text |
| Compact status dot | `md-status-dot` | a colored emoji |
| Read-only value within a known range (quota, battery) | `md-meter` | `md-progress-indicator` |
| A person or entity image/initials | `md-avatar` | a raw `<img>` |
| Touch feedback inside a custom control | `md-ripple` | custom CSS animation |

### 5.6 Data

| Need | Use | Don't use |
|---|---|---|
| Any table | `md-table-container` wrapping `md-table`, with `-head`/`-body`/`-row`/`-cell`/`-foot` inside the table and `-toolbar`/`-pagination` beside it in the container (§7.1) | a native `<table>` |
| Sorting, selection, paging on that table | the same parts — `md-table` carries the STATE (`sort-by`, `sort-order`, `selection`, `row-offset`, `row-count`, `loading`) and emits events; you own the data and do the actual sorting/paging | expecting it to sort an array for you |
| Hierarchy / reporting lines | `md-organization-chart` | nested lists |
| Compare categories | `md-bar-chart` | pie chart |
| Trend over time | `md-line-chart` | bar chart |
| Trend with cumulative volume | `md-area-chart` | line chart |
| Parts of a whole (≤ ~6 slices) | `md-pie-chart` | bar chart |
| Inline micro-trend in a cell or card | `md-sparkline` | a full chart |

> **There is no data-driven table component.** `md-table` is composable: you
> render the rows. It tracks and announces sort, selection and pagination state
> and emits events when the user changes them, but the sorting, filtering and
> slicing of your data is yours to perform. Render rows from your own array in
> response to those events.

### 5.7 Choosing the variant

The matrix picks the component; this picks its shape. Every value below is a
real enum member — anything not listed here is not a valid value.

| Component | Prop | Values, and when |
|---|---|---|
| `md-button` | `variant` | `filled` the one primary action · `tonal` a strong secondary · `outlined` secondary, and the safe choice for a destructive action behind a confirm · `text` low emphasis, dialog Cancel · `elevated` when it sits on a busy or coloured background (default `filled`) |
| `md-button` / `md-icon-button` / `md-button-group` | `size` | `xs` `sm` `md` `lg` `xl` — default `sm`; go `md`/`lg` for consumer CTAs, `xs`/`sm` for dense admin UI |
| `md-icon-button` | `variant` | `standard` (default) · `filled` · `tonal` · `outlined` |
| `md-button-group` | `variant` | `standard` spaced · `connected` fused into one bar |
| `md-fab` | `size` | `standard` (default) · `medium` · `large` |
| `md-fab` | `variant` | `primary-container` (default) · `secondary-container` · `tertiary-container` · `surface` · `primary` · `secondary` · `tertiary` |
| `md-card` | `variant` | `filled` for a group of comparable items · `elevated` (default) for a hero or featured item · `outlined` for a settings or form section |
| `md-text-field` / `md-select` | `variant` | `outlined` for forms on a surface · `filled` for dense or tinted layouts (`md-text-field` defaults to `filled`, `md-select` to `outlined`) — pick one and use it for every field on the screen |
| `md-text-field` | `type` | any native input type: `password`, `email`, `tel`, `url`, `search`, … (default `text`) |
| `md-text-field` | `multiline` | `"auto-grow"` grows with content · `"fixed"` with `rows` for a fixed comment box · `false` (default) single line |
| `md-search` | `variant` | `contained` (default) · `divided` |
| `md-app-bar` | `variant` | `small` (default) · `medium` · `large` for a prominent headline · `search` for a bar that hosts a field |
| `md-chip` | `variant` | `assist` (default) · `filter` for toggleable facets · `input` for user-entered removable values · `suggestion` |
| `md-chip` | `appearance` | `outlined` (default) · `filled` · `elevated` |
| `md-badge` | `variant` | `small` a bare dot · `large` (default) a count |
| `md-tooltip` | `variant` | `plain` (default) a short label on an icon control · `rich` an explanatory popover that may hold a link or action |
| `md-divider` | — | **no `variant`** — use the booleans `inset`, `inset-start`, `inset-end` |
| `md-side-sheet` | `variant` | `standard` coexists with page content · `modal` overlays with a scrim |
| `md-bottom-sheet` | `variant` | `standard` (default) · `detached` floating above the edge |
| `md-dialog` | `fullscreen` | boolean — a full-screen dialog for a long mobile task |
| `md-date-picker` | `variant` | `modal-input` (default) calendar plus a typed field · `modal` calendar only · `docked` inline, anchored to the field |
| `md-time-picker` | `variant` | `dial` · `input` (default) |
| `md-progress-indicator` | `variant` | `linear` (default) · `circular`; add `indeterminate` when the total is unknown |
| `md-list-item` | `lines` | `1` · `2` · `3` — must match how much supporting text you pass |
| `md-navigation-bar` | — | 3–5 destinations. Fewer than three: use `md-tabs`. More than five: a rail or a menu |
| `md-navigation-rail` | — | 3–7 destinations; cap the overflow with `max-visible` |

### 5.8 Not in the library

Rich text editor · file upload/dropzone · calendar/scheduler view · map ·
toast stack manager (use `md-snackbar` and manage the queue yourself) ·
data grid with virtualized columns. If the user needs one, say so plainly and
integrate a third-party component styled with the MD3 tokens.

**There is no `md-grid`, no `md-data-table`, no `md-layout`, no `md-icon`.**
If a tag is not listed in §6, it does not exist — do not emit it.

---

## §6 — Component inventory

**Every component has exactly one manual, and it is the readme.md in its own
source folder:**

```
./packages/core/src/components/<tag>/readme.md
```

e.g. [`md-select`](./packages/core/src/components/md-select/readme.md),
[`md-button`](./packages/core/src/components/md-button/readme.md).

**Load that file before you use the component.** There is no second, shorter
summary to rely on — this readme is the single source, so anything you do not
read there, you do not know. Each one carries When-to-use, a Do/Don't table
from M3, copy-paste patterns, an anti-patterns table, and the theming surface.

Work through them one at a time: pick the component from the decision matrix
(§5), load its readme, write that component's markup, then move to the next.

There are 81 manuals for 57 components: 24 of them document sub-components that
are only valid inside a parent (a table cell, a tab panel, a select option).
Every sub-component manual names its parent in the first line, and §7 below
summarises the nesting.

`status` in each manual's `llm:meta` block is one of:

- **`md3-mapped`** — has a Material Design 3 guidelines page; the Do/Don't is
  sourced from it.
- **`custom`** — an addition to MD3; guidance is derived house rules.
- **`sub-component`** — only valid inside a specific parent.

| Category | Components |
|---|---|
| Actions | `md-button` `md-icon-button` `md-fab` `md-fab-menu` `md-fab-menu-item` `md-split-button` `md-button-group` `md-segmented-button` `md-segmented-button-set` |
| Text input | `md-text-field` `md-number-field` `md-otp-field` `md-autocomplete` `md-search` |
| Selection | `md-select` `md-select-option` `md-multi-select` `md-checkbox` `md-radio` `md-switch` `md-slider` `md-rating` `md-color-picker` `md-transfer-list` `md-chip` |
| Pickers | `md-date-picker` `md-time-picker` |
| Navigation | `md-app-bar` `md-toolbar` `md-navigation-bar` `md-navigation-tab` `md-navigation-rail` `md-navigation-rail-tab` `md-tabs` `md-tab` `md-tab-panels` `md-tab-panel` `md-breadcrumbs` `md-breadcrumb-item` `md-menu` `md-menu-item` `md-menu-item-group` `md-sub-menu-item` `md-stepper` `md-step` |
| Containment | `md-card` `md-dialog` `md-bottom-sheet` `md-side-sheet` `md-snackbar` `md-tooltip` `md-accordion` `md-accordion-item` `md-divider` `md-list` `md-list-item` |
| Data | `md-table` `md-table-container` `md-table-head` `md-table-body` `md-table-row` `md-table-cell` `md-table-foot` `md-table-toolbar` `md-table-pagination` `md-table-sort-label` `md-table-expand-toggle` `md-organization-chart` |
| Charts | `md-bar-chart` `md-line-chart` `md-area-chart` `md-pie-chart` `md-sparkline` |
| Status & feedback | `md-progress-indicator` `md-loading-indicator` `md-skeleton` `md-badge` `md-status-dot` `md-meter` `md-avatar` `md-ripple` |

---

## §7 — Composition rules

### 7.1 What nests inside what

A sub-component is only valid inside its parent. Putting one anywhere else
produces an unstyled, unregistered-looking element with no keyboard behaviour,
because the parent is what wires roving tabindex, ARIA ids and selection.

| Parent | Children it manages |
|---|---|
| `md-button-group` | `md-button`, `md-icon-button` |
| `md-segmented-button-set` | `md-segmented-button` |
| `md-fab-menu` | `md-fab-menu-item` (anchored to an `md-fab` via `anchor="<id>"`) |
| `md-menu` | `md-menu-item`, `md-sub-menu-item`, `md-menu-item-group` — **not** `md-divider`; separate rows with `md-menu-item`'s own `divider` (or `gap`) prop |
| `md-menu-item-group` | `md-menu-item` |
| `md-sub-menu-item` | a nested `md-menu` in `slot="submenu"` — the items go in *that* menu. There is no default slot, so anything else you nest renders nothing |
| `md-select`, `md-multi-select`, `md-autocomplete` | `md-select-option` |
| `md-list` | `md-list-item`, `md-divider` |
| `md-tabs` | `md-tab` |
| `md-tab-panels` | `md-tab-panel` (one per tab, in tab order) |
| `md-navigation-bar` | `md-navigation-tab` |
| `md-navigation-rail` | `md-navigation-rail-tab`, plus an `md-fab` in `slot="fab"` |
| `md-accordion` | `md-accordion-item` |
| `md-stepper` | `md-step` (horizontal steppers also take `slot="content"`) |
| `md-breadcrumbs` | `md-breadcrumb-item` |
| `md-table-container` | `md-table` — **it wraps the table, not the other way round** — plus `md-table-toolbar` in `slot="top"` and `md-table-pagination` in `slot="bottom"`, which sit outside the scroll region |
| `md-table` | `md-table-head`, `md-table-body`, `md-table-foot` (and bare `md-table-row`). It does **not** accept the container, toolbar or pagination |
| `md-table-head` / `-body` / `-foot` | `md-table-row` |
| `md-table-row` | `md-table-cell`, plus `md-table-expand-toggle` for an expandable row |
| `md-table-cell` | `md-table-sort-label` in a header cell |
| `md-tooltip` | **its trigger** — the tooltip wraps the element it describes in its default slot |
| `md-dialog` | body content in the default slot; `md-button` in `slot="actions"` |
| `md-bottom-sheet` / `md-side-sheet` | content in the default slot; `md-button` in `slot="actions"`; headline in `slot="headline"` |
| `md-app-bar` | `md-icon-button` in `slot="leading"` and `slot="trailing"`; `md-menu` for overflow; a field in `slot="search"` on `variant="search"` |
| `md-toolbar` | `md-icon-button`, `md-button`, `md-button-group`; an `md-fab` in `slot="fab"`; `slot="leading"` / `slot="trailing"` for the end clusters |

### 7.2 Pairs that belong together

These don't nest — they sit next to each other in a working flow. Reaching for
one usually means you want the other.

| This | Goes with | Why |
|---|---|---|
| `md-icon-button` | `md-tooltip` | The tooltip supplies the visible meaning the icon lacks |
| `md-button` (`soft-disabled`) | `md-tooltip` | Explains *why* the action is unavailable |
| `md-fab` | `md-fab-menu` | The FAB is the menu's anchor |
| `md-split-button` | `md-menu` | The trailing half opens it |
| `md-card` | `md-button`, `md-icon-button`, `md-divider` | Footer actions, corner action, internal sections |
| `md-list-item` | `md-checkbox`, `md-switch`, `md-icon-button` | Trailing controls in a selectable or settings row |
| `md-bottom-sheet` / `md-side-sheet` | `md-list` | Action menus and filter panels inside the sheet |
| `md-search` | `md-list`, `md-avatar`, `md-icon-button` | Results in the panel; account and voice/filter affordances in the trailing slot |
| `md-date-picker` | `md-time-picker` | Date + time row for booking and scheduling forms |
| `md-time-picker` | `md-button` | The trigger, when `hide-trigger` is set. The picker **is** its own dialog — don't nest it in another one |
| `md-text-field` | `md-button` | Submit / cancel in the form footer |
| `md-multi-select` / `md-autocomplete` | `md-text-field`, `md-menu`, `md-chip` | The field is the trigger (and inherits its variant, density and error state), the menu is the option surface, chips are the selected values |
| `md-autocomplete` | `md-progress-indicator` | The loading row while suggestions are fetched |
| `md-transfer-list` | `md-checkbox`, `md-text-field`, `md-icon-button` | Per-row select, per-side search, mover controls |
| `md-number-field` | `md-text-field` hooks, `md-icon-button` | It *is* an `md-text-field` internally — every `--md-text-field-*` custom property passes through — and its steppers are `md-icon-button`s |
| `md-otp-field` | `md-button` | Verify action (`auto-submit` covers the no-button flow) |
| Any chart | `md-card` | Charts belong on a dashboard tile |
| `md-line-chart` | `md-segmented-button-set` | Period picker (1W / 1M / 1Y) driving the range |
| `md-sparkline` | `md-list-item`, `md-table-cell` | Trend column beside a value |
| `md-skeleton` | `md-card`, `md-list` | Render N placeholders in the real layout while fetching |
| `md-meter` | `md-chip`, `md-card` | Same semantic status colour on both; quota and usage summaries live on a card |
| `md-color-picker` | `md-text-field`, `md-button` | Label and helper text beside it in a form; save / cancel in the surrounding popover or dialog |
| `md-rating` | `md-text-field`, `md-card` | A rating row in a review form; aggregate scores on a review card |
| `md-accordion` | `md-divider` | Optional inner dividers inside long item content |
| `md-stepper` | `md-button` | Next / back / submit adjacent to the stepper |

### 7.3 Nesting that is always wrong

- A dialog opened from inside a dialog. Use `md-stepper` inside **one**
  `md-dialog`.
- A component inside a native interactive element (`<button>`, `<a>`) — it
  nests interactive controls and destroys the accessibility tree. Use the
  component's own `href` / `type` props.
- `md-tabs` used for top-level app navigation. Tabs switch sibling views of the
  same data; destinations are `md-navigation-bar` / `md-navigation-rail`.

---

## §8 — Page recipes

Complete, runnable screens. Each renders as-is once the components are
registered and the token sheet and font links from §3 are loaded — no
placeholder identifiers, no helper functions to write.

### 8.1 Login screen

`<form>` is load-bearing: it is what makes `required` block submit and what
`type="submit"` calls `requestSubmit()` on.

```html
<main style="display: grid; place-items: center; min-block-size: 100dvh; padding: 24px;">
  <form id="login-form" style="inline-size: min(420px, 100%);">
    <md-card variant="elevated" style="padding: 32px; display: flex; flex-direction: column; gap: 20px;">
      <h1 style="margin: 0; font: var(--md-sys-typescale-headline-medium-font);">Sign in</h1>

      <md-text-field
        variant="outlined"
        label="Email"
        type="email"
        name="email"
        autocomplete="username"
        required
      ></md-text-field>

      <md-text-field
        variant="outlined"
        label="Password"
        type="password"
        name="password"
        autocomplete="current-password"
        password-toggle="internal"
        required
      ></md-text-field>

      <md-button variant="filled" type="submit" full-width>Sign in</md-button>
      <md-button variant="text" href="/forgot-password">Forgot password?</md-button>
    </md-card>
  </form>
</main>

<script type="module">
  document.getElementById('login-form').addEventListener('submit', (e) => {
    e.preventDefault();
    const data = new FormData(e.currentTarget);
    console.log(data.get('email'), data.get('password'));
  });
</script>
```

### 8.2 Settings page (mobile)

Top app bar + grouped rows of instant-apply switches. `md-switch` uses
**`selected`**, not `checked`. The rows are `type="text"` (non-interactive), so
the switch is the only control — one tab stop per setting.

```html
<md-app-bar variant="small" headline="Settings">
  <md-icon-button slot="leading" icon="arrow_back" aria-label="Back"></md-icon-button>
</md-app-bar>

<main style="padding: 16px; display: flex; flex-direction: column; gap: 16px;">
  <md-card variant="outlined">
    <md-list>
      <md-list-item headline="Notifications" supporting-text="Push, email, in-app" lines="2">
        <md-switch slot="trailing" selected aria-label="Enable notifications" data-setting="notifications"></md-switch>
      </md-list-item>
      <md-divider></md-divider>
      <md-list-item headline="Dark mode" supporting-text="Match system" lines="2">
        <md-switch slot="trailing" aria-label="Enable dark mode" data-setting="dark"></md-switch>
      </md-list-item>
      <md-divider></md-divider>
      <md-list-item headline="Sync over cellular">
        <md-switch slot="trailing" selected aria-label="Sync over cellular" data-setting="cellular"></md-switch>
      </md-list-item>
    </md-list>
  </md-card>
</main>

<md-snackbar id="settings-toast" message="Setting saved"></md-snackbar>

<script type="module">
  const toast = document.getElementById('settings-toast');
  document.querySelectorAll('md-switch[data-setting]').forEach((sw) => {
    sw.addEventListener('mdChange', (e) => {
      if (sw.dataset.setting === 'dark') {
        document.documentElement.setAttribute('data-theme', e.detail.selected ? 'dark' : 'light');
      }
      toast.show();
    });
  });
</script>
```

### 8.3 Settings form (deferred save, with validation)

When settings are saved on submit rather than applied instantly, use a real
form. `required` blocks the submit; every control needs a `name` to reach
`FormData`.

```html
<form id="profile-form" style="max-inline-size: 560px; margin: 24px auto; padding: 0 16px;">
  <md-card variant="outlined" style="padding: 24px; display: flex; flex-direction: column; gap: 20px;">
    <h2 style="margin: 0; font: var(--md-sys-typescale-title-large-font);">Profile</h2>

    <md-text-field
      variant="outlined"
      label="Display name"
      name="displayName"
      required
      supporting-text="Shown on your public profile"
    ></md-text-field>

    <md-select variant="outlined" label="Language" name="language" value="en" required>
      <md-select-option value="en" label="English"></md-select-option>
      <md-select-option value="de" label="Deutsch"></md-select-option>
      <md-select-option value="ar" label="العربية"></md-select-option>
    </md-select>

    <!-- md-checkbox has NO label slot. It is a labelable, form-associated
         element: wrap it in a native <label> (which names it AND forwards
         clicks), or give the host an aria-label. Text between the tags is
         NOT rendered. -->
    <label style="display: inline-flex; align-items: center; gap: 12px; cursor: pointer;">
      <md-checkbox name="newsletter" value="yes" supporting-text="Monthly, no more"></md-checkbox>
      <span>Send me product news</span>
    </label>

    <div style="display: flex; gap: 8px; justify-content: flex-end;">
      <md-button variant="text" type="reset">Reset</md-button>
      <md-button variant="filled" type="submit">Save changes</md-button>
    </div>
  </md-card>
</form>

<md-snackbar id="saved-toast" message="Changes saved" action="Undo"></md-snackbar>

<script type="module">
  const form = document.getElementById('profile-form');
  const toast = document.getElementById('saved-toast');

  form.addEventListener('submit', (e) => {
    e.preventDefault();
    const data = Object.fromEntries(new FormData(form));
    console.log(data); // { displayName, language, newsletter? }
    toast.show();
  });

  toast.addEventListener('mdAction', () => form.reset());
</script>
```

### 8.4 Dashboard with a FAB

```html
<md-app-bar variant="medium" headline="Inbox">
  <md-icon-button slot="trailing" icon="search" aria-label="Search"></md-icon-button>
  <md-icon-button slot="trailing" icon="filter_list" aria-label="Filter"></md-icon-button>
</md-app-bar>

<main style="padding: 16px; display: grid; gap: 12px; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));">
  <md-card variant="filled" interactive style="padding: 16px;">
    <strong>Acme contract</strong>
    <p style="margin: 4px 0 0; color: var(--md-sys-color-on-surface-variant);">Renewal due in 5 days</p>
  </md-card>
  <md-card variant="filled" interactive style="padding: 16px;">
    <strong>Q4 report draft</strong>
    <p style="margin: 4px 0 0; color: var(--md-sys-color-on-surface-variant);">Shared by Alex</p>
  </md-card>
</main>

<md-fab
  icon="add"
  aria-label="New item"
  style="position: fixed; inset-block-end: 16px; inset-inline-end: 16px; z-index: var(--md-sys-z-index-navigation);"
></md-fab>
```

`inset-inline-end` (not `right`) is what keeps the FAB in the correct corner
under `dir="rtl"`.

### 8.5 Destructive confirmation dialog

`md-dialog` traps focus and returns it to the trigger on close — do not add your
own focus management. The error colouring goes through the button's own custom
properties, never a hex.

```html
<md-button id="open-delete" variant="outlined">Delete account</md-button>

<md-dialog id="confirm-delete" headline="Delete account?" icon="warning">
  <p style="margin: 0;">
    This will permanently delete your account and all associated data.
    This action cannot be undone.
  </p>
  <md-button id="cancel-delete" slot="actions" variant="text">Cancel</md-button>
  <md-button
    id="do-delete"
    slot="actions"
    variant="filled"
    style="--md-button-container-color: var(--md-sys-color-error);
           --md-button-label-color: var(--md-sys-color-on-error);"
  >Delete</md-button>
</md-dialog>

<script type="module">
  const dialog = document.getElementById('confirm-delete');
  document.getElementById('open-delete').addEventListener('mdClick', () => dialog.show());
  document.getElementById('cancel-delete').addEventListener('mdClick', () => dialog.close());
  document.getElementById('do-delete').addEventListener('mdClick', () => {
    dialog.close();
    // perform the deletion
  });
</script>
```

If you omit `slot="actions"` entirely, `md-dialog` renders its own Cancel / OK
pair (labels via `cancel-label` / `ok-label`) and closes itself.

### 8.6 Tabbed content page

`md-tab-panels` finds the nearest preceding `md-tabs` (or the one named by
`for`), follows its `mdTabChange`, and wires `aria-controls` /
`aria-labelledby` both ways. **No JavaScript is required**, and `md-tab` has no
`value` or `selected` prop — selection lives on `md-tabs` as `active-tab-index`.

```html
<md-tabs id="profile-tabs" aria-label="Profile sections" active-tab-index="0">
  <md-tab label="Posts"></md-tab>
  <md-tab label="Replies"></md-tab>
  <md-tab label="Likes" badge="3"></md-tab>
</md-tabs>

<md-tab-panels for="profile-tabs">
  <md-tab-panel style="padding: 16px;">
    <p style="margin: 0;">Your posts appear here.</p>
  </md-tab-panel>
  <md-tab-panel style="padding: 16px;">
    <p style="margin: 0;">Your replies appear here.</p>
  </md-tab-panel>
  <md-tab-panel style="padding: 16px;">
    <p style="margin: 0;">Posts you liked appear here.</p>
  </md-tab-panel>
</md-tab-panels>
```

To react to the switch (lazy-loading a panel, for example), listen on the tabs:

```js
document.getElementById('profile-tabs')
  .addEventListener('mdTabChange', (e) => console.log(e.detail.index, e.detail.previousIndex));
```

### 8.7 The full recipe library

Twenty-two more complete screens live in the documentation, each with a live
demo, the full markup, framework tabs, and production notes. When the user's
request matches one of these, read that page (append the slug to
`https://awc-ui.dev/recipes/`) instead of composing from scratch:

| Recipe slug | Screen |
|---|---|
| `two-factor-verification` | OTP entry, resend countdown, backup-codes dialog |
| `kpi-overview` | Stat tiles with sparklines + charts under a range switcher |
| `data-grid-console` | Bulk-action data table: filters, selection, undo |
| `server-fleet-status` | Table of hosts with meters, sparklines, status dots |
| `release-health-drilldown` | Table row → side-sheet master-detail with charts |
| `csv-import-wizard` | Stepper: upload, map columns, validate, import |
| `role-permission-assignment` | Transfer-list permissions with confirm dialog |
| `org-chart-explorer` | Searchable org chart with side-sheet profiles |
| `moderation-queue` | Verdict controls, confidence meter, card queue |
| `checkout-wizard` | Three-step commerce checkout with validation |
| `appointment-booking` | Date + time pickers inside a stepper flow |
| `survey-nps` | Paged survey using every input control |
| `notification-preferences` | Accordion × switch matrix, quiet hours |
| `app-shell` | Responsive rail/bar chrome, SSR-ready, zero CLS |
| `inbox-shell` | Three-zone list + reading-pane layout |
| `mobile-filter-sheet` | Bottom-sheet filters with applied-chip row |
| `product-reviews` | Rating input + per-star meter breakdown |
| `order-tracking` | Shipment timeline stepper with progress meter |
| `portfolio-markets` | Finance charts + holdings table + trade ticket |
| `pricing-page` | Plan cards, billing toggle, comparison table |
| `async-feedback-patterns` | Skeleton/progress/snackbar choreography rules |
| `charts-gallery` | Every chart component, live-retheming palette |

---

## §9 — Universal do's and don'ts

Apply to every component. Component-specific rules live in each manual.

### 9.1 API and styling

| ✅ Do | ❌ Don't |
|---|---|
| Use the custom element directly — it *is* the control, with its own `href` / `type` props | Wrap it in a native `<button>` / `<a>` (§7.3) |
| Set arrays and objects as **JS properties** (`el.items = [...]`) | Pass them as HTML attributes — they won't parse |
| Theme via `--md-<component>-*` properties, then `::part()` | Reach into shadow internals or override `.md-*` classes |
| Change appearance through design tokens (§4.1) | Hardcode hex colors, px spacing, shadows, or font stacks |
| Use `--md-sys-z-index-*` for your own overlays | Invent z-indexes that land under a menu |
| Use logical CSS (`margin-inline-start`, `inset-inline-end`) | Use `margin-left`, `right`, `padding-right` |
| Set `data-density` / `data-theme` / `dir` once, globally | Set them per component unless you mean a local exception |
| Listen to the component's `md*` events | Rely on native `click` — it fires even when the component's `disabled` / `loading` guard suppressed the action |
| Check the manual for the state prop name | Assume `checked`; `md-switch` uses `selected` |
| Render the element, then call its `@Method` | Call `.show()` on an element not yet in the DOM |

Toggling components flip their own state on activation and *then* emit. On
`md-button`, `mdClick` is cancelable — `preventDefault()` on it vetoes the
toggle and any navigation. `mdChange` fires after the flip and is not
cancelable. If a controlled parent rejects a change it must revert the child
explicitly.

`md-button` has a **toggle mode**: set `toggle` and the button flips its own
`selected` on each activation and exposes `aria-pressed`. `selected` is the
state, not the switch — set it for the initial pressed state and read it back
from `mdClick`'s `detail.selected`. Setting `toggle` and then styling
"pressed" yourself, or setting `selected` without `toggle` (which emits no
`aria-pressed` at all), both produce a control that lies to assistive tech.

### 9.2 Content and hierarchy

| ✅ Do | ❌ Don't |
|---|---|
| Use sentence case for all labels | Uppercase or title-case UI text |
| Keep one high-emphasis action per screen region | Compete `filled` buttons against each other |
| Use `soft-disabled` + `md-tooltip` for contextually-unavailable actions | Use `disabled` and remove it from tab order with no explanation |
| Localize every text prop and `aria-label` | Leave default English prop values in a translated app |

**`disabled` vs `soft-disabled`.** Both render the disabled appearance, but
`disabled` also removes the control from the tab order, so a keyboard or screen
reader user can never find out *why* it is off. `soft-disabled` keeps the
control focusable and announced while still blocking activation — which is what
lets an `md-tooltip` on it explain the gate. Use it whenever the reason is
informative rather than obvious.

Nesting rules — dialog inside a dialog, a component inside a native
`<button>` / `<a>`, tabs used as app navigation — live in §7.3.

### 9.3 Accessibility contract

The library ships WCAG 2.1 AA keyboard and ARIA wiring by default. These are the
rules you must not break:

1. **Every icon-only control gets an accessible name.** `md-icon-button`,
   `md-fab`, and icon-only `md-tab` are nameless without `aria-label` (or
   `aria-labelledby`). `md-fab` will also accept `label`, and warns in the
   console when it has neither.
2. **Every form field gets a `label` prop.** `placeholder` is not a label — it
   disappears on type.
3. **Don't break the tab order.** Every interactive control must be reachable by
   Tab and operable by Enter/Space. If you add `tabindex="-1"` to anything,
   have a reason.
4. **Don't write your own focus trap.** `md-dialog`, `md-bottom-sheet` and
   `md-side-sheet` trap focus while open and restore it to the trigger on
   close — including across shadow boundaries. Adding your own fights theirs.
5. **Don't wrap components in your own live regions.** `md-snackbar` announces
   itself (`politeness="polite"` by default, `"assertive"` when the message must
   interrupt). For an error that blocks the task, use `md-dialog`.
6. **Don't disable motion yourself.** Components honour
   `prefers-reduced-motion: reduce` internally.
7. **Directional icons do not auto-flip in RTL** — see §4.5.
8. **Re-verify contrast after overriding any colour role** — the library's AA
   testing covers the shipped palette only. The Theme Generator at
   <https://awc-ui.dev/theme-generator> runs the WCAG checks live (§4.2).

---

## §10 — Before you ship

1. **Every icon-only control has an accessible name.**
2. **Keyboard-only pass**: reach and operate every control; focus is always
   visible; no traps in dialogs/menus/sheets; focus returns to the trigger.
3. **Theme pass**: light *and* dark, if both are supported — and the attribute
   is set before first paint.
4. **RTL pass**, if an RTL locale ships — check directional icons specifically,
   and grep your own CSS for physical properties.
5. **Density pass** at the configured rung — no clipped labels, touch targets
   still adequate. Remember `density="0"` is inert; use
   `--md-sys-density-scale: 0` to opt a subtree out.
6. **Forms**: every control has a `name`; `required` blocks submit; `FormData`
   contains every field; reset works.
7. **No hardcoded strings** left in markup if the app is localized — including
   default prop values like `Search`, `Dismiss`, `No results`.
8. **No hardcoded design values** — grep the diff for hex colours, `px` radii,
   `box-shadow`, and `z-index` literals.
9. **Contrast** re-verified against AA if colour roles were overridden.
10. **SSR**: if server-rendered, confirm no hydration mismatch and that content
    is present with JS disabled.
11. **No shadow-internal CSS** — only tokens, custom properties, and `::part()`.
12. **Every `md-*` tag you emitted appears in §6.** If it doesn't, it doesn't
    exist.