Skip to content

Stepper

A linear, ordered process broken into steps. It orchestrates slotted md-step children: it owns the active index, the navigation buttons, and whether the user may jump ahead.

Live preview Open in Storybook
Your cart
3 items · $128.40 — step forward to choose delivery.
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-stepper id="st-checkout" label="Checkout progress" style="inline-size: 100%;">
  <md-step label="Cart"></md-step>
  <md-step label="Shipping" description="Address and method"></md-step>
  <md-step label="Payment"></md-step>
  <md-step label="Review"></md-step>
  <div slot="content">
    <div data-panel="0" style="padding: 4px 0 8px; font: var(--md-sys-typescale-body-medium); color: var(--md-sys-color-on-surface-variant);">
      <div style="font: var(--md-sys-typescale-title-small); color: var(--md-sys-color-on-surface);">Your cart</div>
      <div>3 items · $128.40 — step forward to choose delivery.</div>
    </div>
    <div data-panel="1" hidden style="padding: 4px 0 8px; font: var(--md-sys-typescale-body-medium); color: var(--md-sys-color-on-surface-variant);">
      <div style="font: var(--md-sys-typescale-title-small); color: var(--md-sys-color-on-surface);">Shipping</div>
      <div>Standard delivery, 3–5 days. Free over $50.</div>
    </div>
    <div data-panel="2" hidden style="padding: 4px 0 8px; font: var(--md-sys-typescale-body-medium); color: var(--md-sys-color-on-surface-variant);">
      <div style="font: var(--md-sys-typescale-title-small); color: var(--md-sys-color-on-surface);">Payment</div>
      <div>Card ending 4242. Billing address matches shipping.</div>
    </div>
    <div data-panel="3" hidden style="padding: 4px 0 8px; font: var(--md-sys-typescale-body-medium); color: var(--md-sys-color-on-surface-variant);">
      <div style="font: var(--md-sys-typescale-title-small); color: var(--md-sys-color-on-surface);">Review</div>
      <div>Everything looks right? Finishing places the order.</div>
    </div>
  </div>
</md-stepper>

<script type="module">
  var stepper = document.getElementById('st-checkout');
  var panels = Array.prototype.slice.call(document.querySelectorAll('#st-checkout [data-panel]'));
  // A horizontal stepper has ONE content slot — the consumer swaps what is in it.
  // (Vertical steppers are different: content lives inside each md-step.)
  var show = function (i) {
    panels.forEach(function (p) { p.hidden = Number(p.dataset.panel) !== i; });
  };
  stepper.addEventListener('mdStepChange', function (e) { show(e.detail.index); });
  show(Number(stepper.getAttribute('active') || 0));
</script>

Already installed? See the Installation guide for one-time package setup (core + tokens, fonts). Each tab below shows two patterns for using md-stepper 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-stepper) ─── -->
<script type="module">
  import '@awc-ui/core/components/md-stepper';
</script>


<md-stepper></md-stepper>
  • A sequential task with a clear order: checkout, onboarding, a multi-page form, a setup wizard.
  • Steps that need validation before advancing (mdBeforeChange).
  • The user benefits from seeing progress and what remains.
SituationUse instead
Peer views the user switches between freelymd-tabs
Independent collapsible sectionsmd-accordion
Top-level destinationsmd-navigation-bar / md-navigation-rail
Simple progress reportingmd-progress-indicator
A two-field formJust show the form
Hierarchy rather than sequenceBreadcrumbs
NeedSetting
Steps must be done in ordermode="linear" (default)
Any step reachablemode="non-linear" and editable on the steps
Panel per step, expanding in placeorientation="vertical"
One shared panel you swaporientation="horizontal" + the content slot
Minimal dots instead of numbersindicator="dot"
Compact bar for phonesvariant="mobile"
Your own buttonsnav="false"
Block a transitionCancel mdBeforeChange

The demo below runs exactly the handler in its code panel. Continue advances normally from Cart and Shipping; on Payment the gate cancels the move and paints the reason on the step. Back is never gated:

A real mdBeforeChange gate — Continue disables itself while the step is in error Open in Storybook
Press Continue. Cart and Shipping advance normally — Payment is the one that fails.
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-stepper id="wiz" label="Checkout">
  <md-step label="Cart"></md-step>
  <md-step label="Shipping"></md-step>
  <md-step label="Payment"></md-step>
  <md-step label="Review"></md-step>
  <div slot="content">
    <span id="msg">Press Continue. Cart and Shipping advance normally — Payment is the one that fails.</span>
    <md-button id="fix" variant="tonal" size="sm" hidden>Use a valid card</md-button>
  </div>
</md-stepper>

<script type="module">
  const wiz = document.getElementById('wiz');
  const steps = wiz.querySelectorAll('md-step');
  const msg = document.getElementById('msg');
  const fix = document.getElementById('fix');

  // Stand-in for your real validation: the card is bad until the user fixes it.
  let cardValid = false;
  const validateStep = (i) => i !== 2 || cardValid;

  // Offer Continue only when it can succeed: disabled exactly while the CURRENT
  // step is flagged. next-disabled applies wherever the user is, so recompute it
  // on every change — otherwise a valid step is stranded behind a dead button.
  const syncNav = () => { wiz.nextDisabled = !!steps[wiz.active]?.error; };

  wiz.addEventListener('mdBeforeChange', (e) => {
    const { index, previous } = e.detail;
    if (index > previous && !validateStep(previous)) {
      e.preventDefault();                                    // blocks the move
      steps[previous].error = true;                          // and says why
      steps[previous].errorText = 'Enter a valid card number';
      msg.textContent = 'Blocked — mdBeforeChange was cancelled, so Continue is disabled until it passes.';
      fix.hidden = false;
      syncNav();
    } else {
      steps[previous].error = false;                         // clear it once it passes
    }
  });

  wiz.addEventListener('mdStepChange', (e) => {
    msg.textContent = e.detail.index > e.detail.previous
      ? 'That step passed, so the move went through.'
      : 'Going back is never gated.';
    syncNav();
  });

  fix.addEventListener('mdClick', () => {
    cardValid = true;
    steps[2].error = false;
    fix.hidden = true;
    msg.textContent = 'Card accepted — Continue is live again.';
    syncNav();
  });

  syncNav();
</script>

Pair a cancellation with a visible, announced reason — the in-step surface is md-step’s error + error-text. A silent block is invisible to screen-reader users.

mdBeforeChange does not fire for a direct active assignment, which makes active the authoritative commit path after async work:

Async commit — Continue spins for 1.2s, then the move lands Open in Storybook
Press Continue — each move is saved first, then committed.
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-stepper id="wiz" label="Checkout">
  <md-step label="Cart"></md-step>
  <md-step label="Shipping"></md-step>
  <md-step label="Payment"></md-step>
  <md-step label="Review"></md-step>
  <div slot="content">…</div>
</md-stepper>

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

  wiz.addEventListener('mdBeforeChange', async (e) => {
    const target = e.detail.index;
    e.preventDefault();                    // nothing commits yet
    wiz.loading = true;                    // spinner on Continue, Back disabled

    // Direct assignment does NOT re-fire mdBeforeChange, so this cannot loop.
    if (await save()) wiz.active = target;
    wiz.loading = false;
  });
</script>

Use next-disabled for the simpler, declarative case — it disables the built-in Continue / Finish button (not Back, not step clicks) so you can bind it to the current step’s form validity.

auto-complete is on by default: advancing marks the step you leave as completed, so the progress line and check marks just work.

Both steppers below start on step 1 — press Continue a few times in each. The first leaves check marks behind; the second never does, because completion is yours to confirm:

Press Continue in each — only the first leaves check marks Open in Storybook
Default — advancing marks the step you leave as completed.
auto-complete="false" — no check marks until your code sets completed.
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-stepper id="w" auto-complete="false" label="Checkout">
  <md-step label="Cart"></md-step>
  <md-step label="Payment"></md-step>
  <md-step label="Review"></md-step>
</md-stepper>

<script type="module">
  const w = document.getElementById('w');
  const steps = w.querySelectorAll('md-step');

  // Mark the step completed only once the server has actually accepted it.
  w.addEventListener('mdStepChange', async (e) => {
    const { previous } = e.detail;
    if (await saveToServer(previous)) steps[previous].completed = true;
  });
</script>
ModeBehaviour
linear (default)Steps must be done in order; future steps are unreachable until the prior ones are done or optional
non-linearAny step can be selected
Non-linear with editable steps Open in Storybook
Payment
The step you are on. Click a completed header to go back.
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-stepper id="st-nonlinear" mode="non-linear" active="2" label="Non-linear" style="inline-size: 100%;">
  <md-step label="Account" completed editable></md-step>
  <md-step label="Shipping" completed editable></md-step>
  <md-step label="Payment"></md-step>
  <div slot="content">
    <div data-panel="0" hidden style="padding: 4px 0 8px; font: var(--md-sys-typescale-body-medium); color: var(--md-sys-color-on-surface-variant);">
      <div style="font: var(--md-sys-typescale-title-small); color: var(--md-sys-color-on-surface);">Account</div>
      <div>Jump straight back here — an editable step stays clickable.</div>
    </div>
    <div data-panel="1" hidden style="padding: 4px 0 8px; font: var(--md-sys-typescale-body-medium); color: var(--md-sys-color-on-surface-variant);">
      <div style="font: var(--md-sys-typescale-title-small); color: var(--md-sys-color-on-surface);">Shipping</div>
      <div>Also editable, so revisiting it keeps the later progress.</div>
    </div>
    <div data-panel="2" style="padding: 4px 0 8px; font: var(--md-sys-typescale-body-medium); color: var(--md-sys-color-on-surface-variant);">
      <div style="font: var(--md-sys-typescale-title-small); color: var(--md-sys-color-on-surface);">Payment</div>
      <div>The step you are on. Click a completed header to go back.</div>
    </div>
  </div>
</md-stepper>

<script type="module">
  var stepper = document.getElementById('st-nonlinear');
  var panels = Array.prototype.slice.call(document.querySelectorAll('#st-nonlinear [data-panel]'));
  // A horizontal stepper has ONE content slot — the consumer swaps what is in it.
  // (Vertical steppers are different: content lives inside each md-step.)
  var show = function (i) {
    panels.forEach(function (p) { p.hidden = Number(p.dataset.panel) !== i; });
  };
  stepper.addEventListener('mdStepChange', function (e) { show(e.detail.index); });
  show(Number(stepper.getAttribute('active') || 0));
</script>

orientation="horizontal" (default) keeps the step row clean — put the active panel in the stepper’s content slot and swap it on mdStepChange. orientation="vertical" renders each step’s own slotted content in an expanding panel with built-in Back / Continue.

Panels can live in either place, and the stepper picks the layout for you:

Where you put the panelWhat happens
One <div slot="content"> on the stepperThe shared region under the row — you swap it on mdStepChange
Inside each <md-step>Vertical: expands in place. Horizontal: spans the full width under the row
Horizontal with per-step panels — no content slot, no swapping code Open in Storybook
3 items · $128.40
Pick a delivery method.
Card details.
Confirm and pay.
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<!-- horizontal, but each step carries its own panel -->
<md-stepper active="1" label="Checkout">
  <md-step label="Cart">3 items · $128.40</md-step>
  <md-step label="Shipping" description="Address and method">Pick a delivery method.</md-step>
  <md-step label="Payment">Card details.</md-step>
  <md-step label="Review">Confirm and pay.</md-step>
</md-stepper>
Vertical — content renders inside each step Open in Storybook
Pick the repository to deploy. Set the build command. Ship 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-stepper orientation="vertical" label="Deploy" style="inline-size: 100%;">
  <md-step label="Connect repo" icon="cable">Pick the repository to deploy.</md-step>
  <md-step label="Configure build" icon="tune">Set the build command.</md-step>
  <md-step label="Deploy" icon="rocket_launch">Ship it.</md-step>
</md-stepper>

indicator="numbered" (default) draws numbered circles; indicator="dot" draws minimal dots. icon on a step overrides the glyph.

Dot indicator Open in Storybook
Welcome
Panel for “Welcome”. The dot indicator drops the numbers; the content still swaps.
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-stepper id="st-dot" indicator="dot" label="Dot stepper" style="inline-size: 100%;">
  <md-step label="Welcome"></md-step>
  <md-step label="Profile"></md-step>
  <md-step label="Preferences"></md-step>
  <md-step label="Done"></md-step>
  <div slot="content">
    <div data-panel="0" style="padding: 4px 0 8px; font: var(--md-sys-typescale-body-medium); color: var(--md-sys-color-on-surface-variant);">
      <div style="font: var(--md-sys-typescale-title-small); color: var(--md-sys-color-on-surface);">Welcome</div>
      <div>Panel for “Welcome”. The dot indicator drops the numbers; the content still swaps.</div>
    </div>
    <div data-panel="1" hidden style="padding: 4px 0 8px; font: var(--md-sys-typescale-body-medium); color: var(--md-sys-color-on-surface-variant);">
      <div style="font: var(--md-sys-typescale-title-small); color: var(--md-sys-color-on-surface);">Profile</div>
      <div>Panel for “Profile”. The dot indicator drops the numbers; the content still swaps.</div>
    </div>
    <div data-panel="2" hidden style="padding: 4px 0 8px; font: var(--md-sys-typescale-body-medium); color: var(--md-sys-color-on-surface-variant);">
      <div style="font: var(--md-sys-typescale-title-small); color: var(--md-sys-color-on-surface);">Preferences</div>
      <div>Panel for “Preferences”. The dot indicator drops the numbers; the content still swaps.</div>
    </div>
    <div data-panel="3" hidden style="padding: 4px 0 8px; font: var(--md-sys-typescale-body-medium); color: var(--md-sys-color-on-surface-variant);">
      <div style="font: var(--md-sys-typescale-title-small); color: var(--md-sys-color-on-surface);">Done</div>
      <div>Panel for “Done”. The dot indicator drops the numbers; the content still swaps.</div>
    </div>
  </div>
</md-stepper>

<script type="module">
  var stepper = document.getElementById('st-dot');
  var panels = Array.prototype.slice.call(document.querySelectorAll('#st-dot [data-panel]'));
  // A horizontal stepper has ONE content slot — the consumer swaps what is in it.
  // (Vertical steppers are different: content lives inside each md-step.)
  var show = function (i) {
    panels.forEach(function (p) { p.hidden = Number(p.dataset.panel) !== i; });
  };
  stepper.addEventListener('mdStepChange', function (e) { show(e.detail.index); });
  show(Number(stepper.getAttribute('active') || 0));
</script>

variant="mobile" swaps the whole header row for a compact bar — Back, a centered progress readout (dots for indicator="dot", otherwise a “Step N of M” caption), and Continue / Finish. The step headers are hidden, so the active panel must go in the content slot.

A step is completed, error, optional, disabled or plain — and the state shows in the indicator glyph and the announced text, never colour alone.

Completed, error, optional, current, disabled Open in Storybook
Completed, error, optional, current and disabled.
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-stepper mode="non-linear" active="3" label="States" style="inline-size: 100%;">
  <md-step label="Account" completed editable></md-step>
  <md-step label="Shipping" error error-text="Postcode not recognised"></md-step>
  <md-step label="Gift wrap" optional></md-step>
  <md-step label="Payment"></md-step>
  <md-step label="Review" disabled></md-step>
  <div slot="content">Completed, error, optional, current and disabled.</div>
</md-stepper>
variant=mobile — Back, a progress readout, Continue Open in Storybook
Shipping
12 Ampere Way, London — standard delivery.
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<div style="inline-size:100%;max-inline-size:420px;">
  <md-stepper id="st-mobile" variant="mobile" active="1" label="Checkout" style="inline-size: 100%;">
    <md-step label="Account" completed></md-step>
    <md-step label="Shipping"></md-step>
    <md-step label="Payment"></md-step>
    <div slot="content">
      <div data-panel="0" hidden style="padding: 4px 0 8px; font: var(--md-sys-typescale-body-medium); color: var(--md-sys-color-on-surface-variant);">
        <div style="font: var(--md-sys-typescale-title-small); color: var(--md-sys-color-on-surface);">Account</div>
        <div>Signed in as ada@example.com.</div>
      </div>
      <div data-panel="1" style="padding: 4px 0 8px; font: var(--md-sys-typescale-body-medium); color: var(--md-sys-color-on-surface-variant);">
        <div style="font: var(--md-sys-typescale-title-small); color: var(--md-sys-color-on-surface);">Shipping</div>
        <div>12 Ampere Way, London — standard delivery.</div>
      </div>
      <div data-panel="2" hidden style="padding: 4px 0 8px; font: var(--md-sys-typescale-body-medium); color: var(--md-sys-color-on-surface-variant);">
        <div style="font: var(--md-sys-typescale-title-small); color: var(--md-sys-color-on-surface);">Payment</div>
        <div>Card ending 4242. Continue becomes Finish on the last step.</div>
      </div>
    </div>
  </md-stepper>
</div>

<script type="module">
  var stepper = document.getElementById('st-mobile');
  var panels = Array.prototype.slice.call(document.querySelectorAll('#st-mobile [data-panel]'));
  // A horizontal stepper has ONE content slot — the consumer swaps what is in it.
  // (Vertical steppers are different: content lives inside each md-step.)
  var show = function (i) {
    panels.forEach(function (p) { p.hidden = Number(p.dataset.panel) !== i; });
  };
  stepper.addEventListener('mdStepChange', function (e) { show(e.detail.index); });
  show(Number(stepper.getAttribute('active') || 0));
</script>

nav (default true) renders the built-in Back / Continue — under each active step when vertical, as a single bar below the stepper when horizontal. Set nav="false" and you drive the flow with next(), prev() and goTo(index) (all of which still emit the cancelable mdBeforeChange); reset() returns to the first step and clears completed / error state.

The demo below is live — the buttons are ordinary md-buttons calling the stepper’s methods:

nav=false — your buttons, the stepper's methods Open in Storybook
Drive it with the buttons below — they call prev(), next() and reset().
Reset Back Continue
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-stepper id="w" nav="false" label="Checkout">
  <md-step label="Cart"></md-step>
  <md-step label="Payment"></md-step>
  <md-step label="Review"></md-step>
  <div slot="content">…</div>
</md-stepper>

<md-button id="back">Back</md-button>
<md-button id="next" variant="filled">Continue</md-button>

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

  // next() / prev() / goTo() all still emit the cancelable mdBeforeChange,
  // so your validation gate keeps working with your own buttons.
  document.getElementById('back').addEventListener('mdClick', () => w.prev());
  document.getElementById('next').addEventListener('mdClick', () => w.next());
</script>

With nav="false" the button labels are yours too — next-label, back-label and finish-label only name the built-in buttons.

lazy mounts only the active vertical step’s content panel; inactive panels leave layout and the accessibility tree (no collapse animation). Worth it for wizards with heavy per-step forms.

lazy — step through it and watch each panel mount on arrival Open in Storybook
Only this panel is mounted right now. This one mounts when you reach it. And so does this one.
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<!-- Plain HTML has no conditional rendering, so lazy is as far as it goes.
Remove the child yourself if it must truly tear down. -->
<md-stepper id="w" orientation="vertical" lazy label="Deploy">
  <md-step label="Connect repo">…</md-step>
  <md-step label="Configure build">…</md-step>
</md-stepper>

<script type="module">
  const w = document.getElementById('w');
  w.addEventListener('mdStepChange', (e) => {
    // Full teardown: drop the previous step's children entirely.
    w.querySelectorAll('md-step')[e.detail.previous].replaceChildren();
  });
</script>
Fully localized — including the announced words Open in Storybook
Votre panier contient 3 articles.
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-stepper label="Progression" step-word="Étape" of-word="sur"
  completed-word="terminée" current-word="actuelle" error-word="erreur"
  optional-word="Facultatif" next-label="Continuer" back-label="Retour"
  finish-label="Terminer" style="inline-size: 100%;">
  <md-step label="Panier"></md-step>
  <md-step label="Livraison" optional></md-step>
  <md-step label="Paiement"></md-step>
  <div slot="content">Votre panier contient 3 articles.</div>
</md-stepper>

The optional-word is rendered by each md-step marked optional, so that string is translated here on the parent, not on the step. Per-step strings (label, description, error-text, accessible-name) are translated on the step. The assembled order (“Step 2 of 4”) is fixed — locales needing a different order may need the wording adjusted to fit.

EventCancelableDetailFires
mdBeforeChangeyes{ index, previous }Before a user-driven move commits — not on a direct active assignment
mdStepChangeno{ index, previous }After the move commits
mdCompletenovoidContinue pressed on the last step
All three events, live — press Continue to the end Open in Storybook
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-stepper id="wiz" label="Checkout progress">
  <md-step label="Account"></md-step>
  <md-step label="Payment"></md-step>
  <md-step label="Review"></md-step>
  <div slot="content" id="panel">Account details…</div>
</md-stepper>

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

  // Cancelable, before the move commits — this is the validation gate.
  wiz.addEventListener('mdBeforeChange', (e) => {
    const { index, previous } = e.detail;
    if (index > previous && !validateStep(previous)) e.preventDefault();
  });

  // After the move commits — swap the shared panel here.
  wiz.addEventListener('mdStepChange', (e) => renderPanel(e.detail.index));

  // Continue pressed on the last step.
  wiz.addEventListener('mdComplete', () => submitOrder());
</script>

Properties

PropertyAttributeTypeDefaultReflects
orientationorientation'horizontal' | 'vertical''horizontal'Yes
indicatorindicator'numbered' | 'dot''numbered'Yes
variantvariant'default' | 'mobile''default'Yes
modemode'linear' | 'non-linear''linear'Yes
activeactivenumber0Yes
autoCompleteauto-completebooleantrue
navnavbooleantrue
nextDisablednext-disabledbooleanfalse
loadingloadingbooleanfalse
lazylazybooleanfalse
labellabelstring'Progress'
stepWordstep-wordstring'Step'
ofWordof-wordstring'of'
completedWordcompleted-wordstring'completed'
currentWordcurrent-wordstring'current'
errorWorderror-wordstring'error'
optionalWordoptional-wordstring'Optional'
nextLabelnext-labelstring'Continue'
backLabelback-labelstring'Back'
finishLabelfinish-labelstring'Finish'
densitydensity0 | -1 | -2 | -3 | -40Yes

Methods

MethodParameters
next()none
prev()none
goTo()index: number
reset()none

Slots

SlotDescription
(default)
contentActive-step panel for horizontal / mobile steppers — rendered

CSS Custom Properties

Override on the host element for per-instance theming:

PropertyDescription
--md-stepper-gapSpacing between steps (vertical orientation only).
--md-stepper-step-count

CSS Shadow Parts

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

PartDescription
list
content
nav
mobile-nav
mobile-progress

A stepper is composed from one sub-component, documented here rather than on its own page: it means nothing outside md-stepper, which owns the active index, completion and the navigation events.

One step in the stepper — an indicator (number, dot, check or error glyph), a label, an optional description, and the step’s content.

  • active and completed are stepper-managed. The stepper’s active index drives active, and its auto-complete drives completed. Set completed yourself only after turning auto-complete="false" off on the parent — otherwise the two fight.
  • mdStepClick, mdStepNext and mdStepBack are internal plumbing behind the header and the built-in action buttons; the stepper consumes (and stops) them. Application logic belongs on the stepper’s mdBeforeChange / mdStepChange / mdComplete.
  • error + error-text is the in-step validation surface — pair it with cancelling the stepper’s mdBeforeChange so the block has a visible, announced reason. error-text replaces description while the error is set.
  • In mode="linear", a finished step is only revisitable if you set editable — which, with auto-complete on, also preserves downstream completion when the user edits in place. Revisiting a non-editable step restarts progress from there.
  • optional renders the optional-word supplied by the stepper, so that string is translated once on the parent rather than per step.

Properties

PropertyAttributeTypeDefaultReflects
labellabelstring''
descriptiondescriptionstring''
completedcompletedbooleanfalseYes
activeactivebooleanfalseYes
errorerrorbooleanfalseYes
errorTexterror-textstring''
disableddisabledbooleanfalseYes
optionaloptionalbooleanfalseYes
editableeditablebooleanfalseYes
iconiconstring''
completedIconcompleted-iconstring'check'
errorIconerror-iconstring'priority_high'
accessibleNameaccessible-namestring
hideActionshide-actionsbooleanfalseYes
densitydensity0 | -1 | -2 | -3 | -40Yes

Slots

SlotDescription
(default)

CSS Custom Properties

Override on the host element for per-instance theming:

PropertyDescription
--md-step-indicator-sizeBubble diameter (32px; 28px compact)
--md-step-indicator-ringHover / ripple ring thickness around the
--md-step-indicator-colorPending bubble background
--md-step-indicator-textPending bubble text
--md-step-active-colorActive bubble background
--md-step-active-textActive bubble text
--md-step-active-halo-colorSoft halo around the active bubble
--md-step-completed-colorCompleted bubble background
--md-step-completed-textCompleted bubble text
--md-step-error-colorError bubble background
--md-step-error-textError bubble text
--md-step-connector-colorInactive connector track
--md-step-connector-filled-colorFilled connector
--md-step-connector-thicknessConnector thickness (4px)
--md-step-connector-durationConnector fill duration
--md-step-label-colorResting label colour
--md-step-label-active-colorActive label colour
--md-step-description-colorSupporting / optional caption colour
--md-step-state-layer-colorHover / focus / press overlay
--md-step-expand-durationVertical panel expand duration
--md-step-collapse-durationVertical panel collapse duration
--md-step-idx

CSS Shadow Parts

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

PartDescription
bubble
dot
connector connector-leading
inner
indicator
state-layer
text
label
optional
description
connector connector-trailing
content
actions
  • label names the progress landmark. step-word / of-word / completed-word / current-word / error-word / optional-word are assembled into each step’s announcement (“Step 2 of 4, current”).
  • The current step is exposed as current; completed and error states are conveyed by icon and text, not by colour alone.
  • Cancelling mdBeforeChange should be paired with a visible, announced reason — a silent block is invisible to screen-reader users.
  • With nav="false" you own the navigation buttons and their accessible names.
  • Keep DOM order equal to step order.
KeyAction
TabThe step headers (when reachable), then the navigation buttons
/ Move between reachable headers on a horizontal stepper (mirrored under dir="rtl")
/ Move between headers on a vertical stepper
Home / EndFirst / last reachable step
Enter / SpaceSelect the focused step — still subject to mdBeforeChange
Roving focus across reachable steps Open in Storybook
Tab to the step headers, then arrow between them.
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-stepper mode="non-linear" active="1" label="Checkout progress" style="inline-size: 100%;">
  <md-step label="Account" description="Signed in as ada@example.com" completed editable></md-step>
  <md-step label="Shipping" description="Where it goes"></md-step>
  <md-step label="Gift wrap" description="Skippable" optional></md-step>
  <md-step label="Payment" description="Card details"></md-step>
  <div slot="content">Tab to the step headers, then arrow between them.</div>
</md-stepper>

RTL — the horizontal layout, connectors and navigation buttons mirror under dir="rtl", and the horizontal arrow keys invert with them. See RTL.

Same markup, dir=ltr vs dir=rtl
Content
المحتوى
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;gap:24px;inline-size:100%;">
  <div dir="ltr">
    <md-stepper active="1" label="Checkout" style="inline-size: 100%;">
      <md-step label="Account" completed></md-step>
      <md-step label="Shipping"></md-step>
      <md-step label="Payment"></md-step>
      <div slot="content">Content</div>
    </md-stepper>
  </div>
  <div dir="rtl">
    <md-stepper active="1" label="الدفع" step-word="خطوة" of-word="من" style="inline-size: 100%;">
      <md-step label="الحساب" completed></md-step>
      <md-step label="الشحن"></md-step>
      <md-step label="الدفع"></md-step>
      <div slot="content">المحتوى</div>
    </md-stepper>
  </div>
</div>

Densitydensity="-1…-4" compacts indicators, connectors and content, and overrides an inherited data-density ancestor. See Density.

Density 0 through -4 Open in Storybook
0 -1 -2 -3 -4
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:18px 16px;align-items:center;inline-size:100%;">
  <span style="inline-size:2rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">0</span>
  <md-stepper density="0" active="1" label="d0" nav="false" style="inline-size:100%;"><md-step label="Account" completed></md-step><md-step label="Shipping"></md-step><md-step label="Payment"></md-step></md-stepper>
  <span style="inline-size:2rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">-1</span>
  <md-stepper density="-1" active="1" label="d-1" nav="false" style="inline-size:100%;"><md-step label="Account" completed></md-step><md-step label="Shipping"></md-step><md-step label="Payment"></md-step></md-stepper>
  <span style="inline-size:2rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">-2</span>
  <md-stepper density="-2" active="1" label="d-2" nav="false" style="inline-size:100%;"><md-step label="Account" completed></md-step><md-step label="Shipping"></md-step><md-step label="Payment"></md-step></md-stepper>
  <span style="inline-size:2rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">-3</span>
  <md-stepper density="-3" active="1" label="d-3" nav="false" style="inline-size:100%;"><md-step label="Account" completed></md-step><md-step label="Shipping"></md-step><md-step label="Payment"></md-step></md-stepper>
  <span style="inline-size:2rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">-4</span>
  <md-stepper density="-4" active="1" label="d-4" nav="false" style="inline-size:100%;"><md-step label="Account" completed></md-step><md-step label="Shipping"></md-step><md-step label="Payment"></md-step></md-stepper>
</div>

i18n — translate every *-word and *-label on the stepper, plus each step’s label, description and error-text. See the i18n section above.

The stepper owns one property — the gap between steps. Everything visible belongs to md-step, and those properties cascade from the stepper.

Custom propertyOnPurpose
--md-stepper-gapmd-stepperSpace between steps
--md-step-indicator-sizemd-stepIndicator diameter
--md-step-indicator-color / -indicator-text / -indicator-ringmd-stepResting indicator fill, number, ring
--md-step-active-color / -active-text / -active-halo-colormd-stepCurrent step indicator
--md-step-completed-color / -completed-textmd-stepCompleted indicator
--md-step-error-color / -error-textmd-stepError indicator
--md-step-connector-color / -connector-filled-color / -connector-thicknessmd-stepThe rule between steps
--md-step-label-color / -label-active-colormd-stepLabel text
--md-step-description-colormd-stepDescription text
--md-step-state-layer-colormd-stepHover / focus / press overlay
--md-step-expand-duration / -collapse-duration / -connector-durationmd-stepMotion
Tonal palette, larger indicators, and a branded stepper Open in Storybook
Tonal palette
Larger indicators and a thicker connector
Branded
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;gap:24px;inline-size:100%;">
  <md-stepper active="1" label="Tonal" style="inline-size:100%; --md-step-active-color: var(--md-sys-color-tertiary); --md-step-active-text: var(--md-sys-color-on-tertiary); --md-step-completed-color: var(--md-sys-color-tertiary-container); --md-step-completed-text: var(--md-sys-color-on-tertiary-container); --md-step-connector-filled-color: var(--md-sys-color-tertiary);">
    <md-step label="Account" completed></md-step>
    <md-step label="Shipping"></md-step>
    <md-step label="Payment"></md-step>
    <div slot="content">Tonal palette</div>
  </md-stepper>

  <md-stepper active="1" label="Big indicators" style="inline-size:100%; --md-stepper-gap: 32px; --md-step-indicator-size: 44px; --md-step-connector-thickness: 3px;">
    <md-step label="Account" completed></md-step>
    <md-step label="Shipping"></md-step>
    <md-step label="Payment"></md-step>
    <div slot="content">Larger indicators and a thicker connector</div>
  </md-stepper>

  <md-stepper active="1" label="Branded" style="inline-size:100%; --md-step-active-color: #7c4dff; --md-step-active-text: #ffffff; --md-step-completed-color: #7c4dff; --md-step-completed-text: #ffffff; --md-step-connector-filled-color: #7c4dff; --md-step-label-active-color: #311b92;">
    <md-step label="Account" completed></md-step>
    <md-step label="Shipping"></md-step>
    <md-step label="Payment"></md-step>
    <div slot="content">Branded</div>
  </md-stepper>
</div>

Every default resolves through an md-sys-color role, so a stepper that sets no custom properties follows the theme on its own:

Untouched defaults — follows the page theme Open in Storybook
Switch this page between light and dark.
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-stepper active="1" label="Untouched defaults" style="inline-size: 100%;">
  <md-step label="Account" description="Signed in" completed></md-step>
  <md-step label="Shipping" description="Where it goes"></md-step>
  <md-step label="Payment" description="Card details"></md-step>
  <div slot="content">Switch this page between light and dark.</div>
</md-stepper>
PartOnElement
listmd-stepperThe step header row / column
contentmd-stepperThe shared content slot wrapper
navmd-stepperThe built-in Back / Continue row
mobile-nav / mobile-progressmd-steppervariant="mobile" bar and its readout
indicator / bubble / dot / innermd-stepThe indicator, by shape
connector connector-leading / connector connector-trailingmd-stepThe rules either side
text / label / description / optionalmd-stepThe text block
state-layermd-stepHover / focus / press overlay
content / actionsmd-stepVertical-mode panel and its buttons
nav, list, label and description
The nav row is right-aligned via ::part(nav).
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<style>
  .parts-stepper::part(nav) { justify-content: flex-end; }
  .parts-stepper::part(list) { padding-block-end: 8px; border-block-end: 1px solid var(--md-sys-color-outline-variant); }
  .parts-stepper md-step::part(label) { text-transform: uppercase; letter-spacing: .08em; font-size: 11px; }
  .parts-stepper md-step::part(description) { font-style: italic; }
</style>
<md-stepper class="parts-stepper" active="1" label="Styled parts" style="inline-size: 100%;">
  <md-step label="Account" description="Signed in" completed></md-step>
  <md-step label="Shipping" description="Where it goes"></md-step>
  <md-step label="Payment" description="Card details"></md-step>
  <div slot="content">The nav row is right-aligned via ::part(nav).</div>
</md-stepper>
md-stepper::part(nav) {
justify-content: flex-end;
}

md-step · md-tabs · md-progress-indicator · md-button

For AI Agents — md-stepper

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-stepper 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-stepper readme.md

# md-stepper

<!-- llm:meta
tag: md-stepper
category: navigation
status: custom
m3-guidelines: none — M3 has no stepper page
m3-derived-from: https://m3.material.io/components/progress-indicators/guidelines, https://m3.material.io/components/tabs/guidelines
form-associated: false
depends-on: md-button
used-by: none
accepts-children: md-step
-->

**A sequential process broken into ordered steps.** Owns the active index, the
built-in navigation and whether the user may jump ahead, and pushes layout plus
every localized word down to its slotted `md-step` children.

> ⚠️ **Not a Material Design 3 component.** M3 has no stepper page; the guidance
> below is house rules informed by M3's progress-indicator and tab rules.

> Setup, theming, density and i18n are configured once for the whole library —
> see [`main-llm.md`](../../../../../main-llm.md) at the repo root.

---

## When to use

- A **sequential** task with a clear order: checkout, onboarding, a multi-page
  form, a setup wizard.
- Steps that must validate before the user advances (`mdBeforeChange` or
  `next-disabled`).
- The user benefits from seeing progress and what remains.

## When NOT to use

| Situation | Use instead |
|---|---|
| Peer views the user switches between freely | `md-tabs` |
| Independent collapsible sections | `md-accordion` |
| Top-level destinations | `md-navigation-bar` / `md-navigation-rail` |
| Simple progress reporting | `md-progress-indicator` |
| A two-field form | Just show the form |
| Hierarchy, not sequence | `md-breadcrumbs` |

## Decision cues

| Need | Setting |
|---|---|
| Steps must be done in order | `mode="linear"` (default) |
| User may jump to any step | `mode="non-linear"` |
| Numbered circles | `indicator="numbered"` (default) |
| Minimal dots | `indicator="dot"` |
| Compact bar for narrow screens | `variant="mobile"` (+ a `content` slot) |
| Vertical layout with inline panels | `orientation="vertical"` |
| A panel per step (either orientation) | Put content inside each `md-step` |
| One shared panel you swap yourself | Put it in the stepper's `content` slot |
| Mount only the active step's panel | `lazy` |
| Gate Continue on the current form's validity | `next-disabled` |
| Async work behind Continue | `loading` |
| Supply your own buttons | `nav="false"` |
| Block a specific transition | Cancel `mdBeforeChange` |

## API contract

```html
<md-stepper
  orientation="horizontal|vertical"    <!-- default: horizontal -->
  indicator="numbered|dot"             <!-- default: numbered -->
  variant="default|mobile"             <!-- default: default -->
  mode="linear|non-linear"             <!-- default: linear -->
  active="0"                           <!-- default: 0, out-of-range clamps -->
  auto-complete="true"                 <!-- default: true -->
  nav="true"                           <!-- default: true -->
  next-disabled="false"                <!-- default: false -->
  loading="false"                      <!-- default: false -->
  lazy="false"                         <!-- default: false -->
  label="Progress"                     <!-- default: Progress -->
  step-word="Step"                     <!-- default: Step -->
  of-word="of"                         <!-- default: of -->
  completed-word="completed"           <!-- default: completed -->
  current-word="current"               <!-- default: current -->
  error-word="error"                   <!-- default: error -->
  optional-word="Optional"             <!-- default: Optional -->
  next-label="Continue"                <!-- default: Continue -->
  back-label="Back"                    <!-- default: Back -->
  finish-label="Finish"                <!-- default: Finish -->
  density="-1|-2|-3|-4"                <!-- omit for the default rung -->
>
  <md-step label="Account">Account fields go here.</md-step>
  <md-step label="Payment">Payment fields go here.</md-step>
  <md-step label="Review">Summary goes here.</md-step>
</md-stepper>
```

**Events** — all three bubble and are composed, so you may listen on the
stepper or on an ancestor.

| Event | Cancelable | Detail | Fires |
|---|---|---|---|
| `mdBeforeChange` | **yes** | `{ index, previous }` | Before a user-driven move commits |
| `mdStepChange` | no | `{ index, previous }` | After `active` actually changed |
| `mdComplete` | no | `void` | Continue pressed on the last step |

**Methods** — `next()`, `prev()`, `goTo(index)`, `reset()`. All are async and
resolve to `void`.

**Slots** — the default slot takes `md-step` children; the named `content` slot
takes one shared panel rendered between the step row and the nav bar.

**Parts** — `list`, `content`, `nav`, `mobile-nav`, `mobile-progress`.

**Written by the component — never set these by hand:** the `data-panel-mode`
attribute and the `--md-stepper-step-count` variable on the host, and the
`data-index` / `data-total` / `data-active` / `data-orientation` /
`data-indicator` / `data-mode` / `data-position` / `data-nav` / `data-lazy` /
`data-loading` / `data-next-disabled` / `data-*-word` / `data-*-label`
attributes plus `--md-step-idx` that the stepper stamps on each `md-step`.

### Behavioral contract worth knowing

- **Steps must be direct children.** The stepper collects `md-step` elements
  from its own child list only — wrap them in a `<div>` and the step count is
  0, no nav bar renders and nothing is navigable.
- `active` clamps into `0 … count - 1` (and to `0` when there are no steps),
  including the initial attribute value.
- **`mdBeforeChange` is the validation hook.** It fires for step clicks, the
  built-in Back / Continue, and `next()` / `prev()` / `goTo()`. Cancel it to
  keep the user where they are.
- **Assigning `active` yourself does not emit `mdBeforeChange`** — it still
  emits `mdStepChange` and still applies auto-complete. That is the sanctioned
  way to commit a transition you vetoed while you awaited something. Never call
  `next()` or `goTo()` from inside a handler that cancelled `mdBeforeChange`:
  they re-emit it and re-enter your handler.
- `auto-complete` (default **on**) moving **forward** marks every non-disabled
  step you passed over as `completed` and clears its `error`. Moving
  **backward** it un-completes the target and everything after it, so a later
  step can never stay checked while an earlier one is not — unless the target
  is `editable` **and** already `completed`, which is review-in-place and
  leaves downstream completion alone.
- `next()` on the last reachable step does not move: with `auto-complete` on it
  marks the current step completed, then emits `mdComplete`. `next()` and
  `prev()` skip `disabled` steps.
- `mode="linear"` gating: backward is always allowed; forward is allowed only
  when the target is already `completed`, or every earlier step is `completed`
  or `optional`. Unreachable step headers are `aria-disabled` and out of the
  tab order. `mode="non-linear"` allows any non-disabled step.
- **`next-disabled` and `loading` gate only the built-in Continue button.**
  Clicking a step header and calling `next()` / `goTo()` are unaffected — use
  `mode="linear"` and/or `mdBeforeChange` for those. `loading` also disables
  Back, which is what stops a double-submit.
- **Where the nav lives depends on orientation.** A horizontal stepper renders
  one Back / Continue bar below the content; a vertical stepper has each active
  step render its own Back / Continue inside its panel. Both are suppressed by
  `nav="false"`, and a vertical step opts out individually with `hide-actions`.
- `variant="mobile"` hides the entire step-header row and renders the compact
  Back · progress · Continue bar instead, so the active panel must go in the
  stepper's `content` slot. That bar is part of the navigation: with
  `nav="false"` nothing is rendered but the content slot.
- `lazy` mounts only the active step's panel; the others are out of layout and
  out of the a11y tree, with no collapse animation. It drops the *panel*, not
  your component — slotted custom elements stay connected in the light DOM, so
  a real teardown means conditionally rendering the content in your framework.
- **Panels live in one of two places, and the stepper picks the layout.** Put
  content inside each `md-step` and it panels per step — expanding in place when
  vertical, spanning the full width under the row when horizontal. Put one
  `<div slot="content">` on the stepper instead and you own a single region you
  swap on `mdStepChange`.
- **Per-step panels are detected, not configured.** There is no prop: the moment
  any step has light-DOM content, a horizontal stepper switches its row from a
  flex line to a grid so the panel can span every column underneath. A
  horizontal stepper whose steps are all empty is unaffected.

  This is why two placements exist at all — a slot cannot reach a *grandchild*,
  so `md-stepper` can never project a step's children into its own `content`
  region. Only `md-step` can render them, and the grid is what lets its panel
  escape a `flex: 1 1 0` column that would otherwise be 1/N of the row.
- The built-in buttons are internal chrome: their composed `mdClick` retargets
  to the stepper's host and is stopped there, so a delegated listener on any
  ancestor never sees Back or Continue. `stopPropagation()` does not silence
  listeners bound to the `<md-stepper>` element itself — those still fire for
  the built-in buttons, so don't delegate off the host. Listen for
  `mdStepChange` / `mdComplete` instead.

---

## Do / Don't

House rules, informed by
[M3 · Progress indicators](https://m3.material.io/components/progress-indicators/guidelines)
and [M3 · Tabs](https://m3.material.io/components/tabs/guidelines).

| ✅ Do | ❌ Don't |
|---|---|
| Use a stepper only for genuinely sequential work | Don't use one for peer views — that's tabs |
| Validate in `mdBeforeChange` and cancel on failure | Don't let users advance past an invalid step silently |
| Keep step labels to one or two words | Don't write sentences as step labels |
| Mark genuinely skippable steps `optional` | Don't mark everything optional |
| Show errors on the step with `error` + `error-text` | Don't surface step errors only in a dialog |
| Localize every `*-word` and `*-label` | Don't ship the English defaults |
| Use `mode="non-linear"` when order really doesn't matter | Don't force linear order artificially |
| Keep the step count small (3–5) | Don't build a twelve-step wizard |
| Use `loading` for async transitions | Don't leave the user unsure whether Continue worked |
| Put the panel inside each step when it belongs to that step | Don't hand-roll a `content` slot and swapping code you don't need |

---

## Patterns

```html
<!-- Gate every forward move on the current step's validity -->
<md-stepper id="wiz" label="Checkout progress">
  <md-step label="Account">Account fields go here.</md-step>
  <md-step label="Payment">Payment fields go here.</md-step>
  <md-step label="Review">Summary goes here.</md-step>
</md-stepper>

<script type="module">
  const wiz = document.getElementById('wiz');
  const validateStep = (index) => true; // your real validation

  wiz.addEventListener('mdBeforeChange', (e) => {
    const { index, previous } = e.detail;
    if (index > previous && !validateStep(previous)) e.preventDefault();
  });

  wiz.addEventListener('mdStepChange', (e) => console.log(e.detail.index));
  wiz.addEventListener('mdComplete', () => console.log('finished'));
</script>
```

```html
<!-- Async work behind Continue: veto, await, then COMMIT by setting `active`
     (calling next()/goTo() here would re-enter this handler). -->
<md-stepper id="signup" label="Sign-up">
  <md-step label="Email">Email field goes here.</md-step>
  <md-step label="Profile">Profile fields go here.</md-step>
</md-stepper>

<script type="module">
  const signup = document.getElementById('signup');
  const save = async () => true; // your real request

  signup.addEventListener('mdBeforeChange', async (e) => {
    e.preventDefault();
    signup.loading = true;
    const ok = await save();
    signup.loading = false;
    if (ok) signup.active = e.detail.index;
  });
</script>
```

```html
<!-- Declarative gate: bind next-disabled to the current form's validity -->
<md-stepper id="form-wiz" next-disabled label="Details">
  <md-step label="Name">
    <md-text-field id="name" label="Full name" required></md-text-field>
  </md-step>
  <md-step label="Done">All set.</md-step>
</md-stepper>

<script type="module">
  const formWiz = document.getElementById('form-wiz');
  const name = document.getElementById('name');
  name.addEventListener('mdInput', () => {
    formWiz.nextDisabled = !name.value.trim();
  });
</script>
```

```html
<!-- Vertical wizard: each step owns its panel and its Back / Continue -->
<md-stepper orientation="vertical" label="Setup">
  <md-step label="Workspace">Name your workspace.</md-step>
  <md-step label="Members" optional>Invite teammates.</md-step>
  <md-step label="Finish">Review and finish.</md-step>
</md-stepper>
```

```html
<!-- Mobile bar: headers are hidden, so the panel lives in the content slot -->
<md-stepper id="mob" variant="mobile" indicator="dot" label="Onboarding">
  <md-step label="Welcome"></md-step>
  <md-step label="Profile"></md-step>
  <md-step label="Done"></md-step>
  <div slot="content" id="mob-panel">Welcome!</div>
</md-stepper>

<script type="module">
  const mob = document.getElementById('mob');
  const panels = ['Welcome!', 'Tell us about you.', 'All done.'];
  mob.addEventListener('mdStepChange', (e) => {
    document.getElementById('mob-panel').textContent = panels[e.detail.index];
  });
</script>
```

```html
<!-- Your own navigation -->
<md-stepper id="own" nav="false" label="Manual">
  <md-step label="One">First.</md-step>
  <md-step label="Two">Second.</md-step>
</md-stepper>
<md-button id="own-back">Back</md-button>
<md-button id="own-next" variant="filled">Continue</md-button>

<script type="module">
  const own = document.getElementById('own');
  document.getElementById('own-back').addEventListener('click', () => own.prev());
  document.getElementById('own-next').addEventListener('click', () => own.next());
</script>
```

```html
<!-- Localized: every user-facing word is a prop -->
<md-stepper
  label="Progression"
  step-word="Étape" of-word="sur"
  completed-word="terminée" current-word="actuelle" error-word="erreur"
  optional-word="Facultatif"
  next-label="Continuer" back-label="Retour" finish-label="Terminer"
>
  <md-step label="Compte">Champs du compte.</md-step>
  <md-step label="Paiement">Champs de paiement.</md-step>
</md-stepper>
```

## Anti-patterns

| ❌ Wrong | ✅ Right | Why |
|---|---|---|
| Wrapping the steps in a `<div>` or a `<form>` element | Make every `md-step` a direct child | Only direct children are collected — a wrapper makes the stepper empty. |
| Validating in `mdStepChange` | Validate in `mdBeforeChange` and cancel | By `mdStepChange` the move already happened. |
| Calling `next()` / `goTo()` to commit after cancelling `mdBeforeChange` | Assign `stepper.active = index` | The methods re-emit `mdBeforeChange` and re-enter your handler. |
| Expecting `next-disabled` to block step clicks | Use `mode="linear"` or cancel `mdBeforeChange` | It only disables the built-in Continue button. |
| Leaving `auto-complete` on when completion needs server confirmation | Set `auto-complete="false"` and set `completed` yourself | Otherwise steps look done before they are. |
| Assuming a completed step stays completed after stepping back | Set `editable` on steps meant for review | Backward moves un-complete the target and everything after it. |
| `variant="mobile"` with content inside each `md-step` | Put the panel in the stepper's `content` slot | The mobile variant hides the whole step row, panels included. |
| `variant="mobile"` together with `nav="false"` | Keep `nav` on, or drop the mobile variant | The mobile bar *is* the navigation — nothing renders without it. |
| Per-step panels **and** a shared `content` slot on one stepper | Pick one | Both render — per-step panels inside the row, the shared region below it. |
| Expecting `lazy` to destroy a slotted component | Conditionally render it | `lazy` drops the panel; the slotted element stays connected. |
| Setting `data-active` / `data-index` / `--md-stepper-step-count` by hand | Set `active`, `orientation`, … | Those are written by the stepper and overwritten on every sync. |
| Shipping the English `*-word` props in a localized app | Translate them all | They're assembled into the announcements. |
| `density="0"` to escape an inherited rung | `style="--md-sys-density-scale: 0"` | There is no rung 0 — the attribute is inert. |
| A stepper for tabbed views | `md-tabs` | Steppers imply order. |
| A twelve-step wizard | Group into 3–5 phases | Cognitive load. |

## Accessibility, RTL, density, i18n

**Accessibility** — the host is a `navigation` landmark named by `label`
(default `Progress`), wrapping an ordered list of step headers. Each header is
a `button` with `aria-current="step"` when active, `aria-disabled` when
unreachable or disabled, and `aria-expanded` when it owns a panel. There is no
roving tabindex: every reachable header is its own tab stop, activated with
`Enter` or `Space`; unreachable and disabled headers are removed from the tab
order. Two visually-hidden live regions announce changes — a polite one for
"Step 2 of 4: Payment, current" and an assertive one when a step enters
`error`. After the built-in nav moves a vertical stepper, focus lands on the
new active header rather than falling to `<body>`. Cancelling `mdBeforeChange`
should be paired with a visible, announced reason — a silent block is invisible
to screen-reader users. Keep DOM order equal to step order.

**RTL** — the layout is built on logical properties, so the row, the connectors
and the nav bar mirror under `dir="rtl"`, including the direction the connector
fill grows in.

**Density** — `density="-1…-4"` locally overrides the inherited `data-density`
rung; only those four rungs exist, and omitting the attribute is the
uncompacted default. It compacts the indicators, connector thickness and the
nav spacing, and cascades to the slotted steps. `density="0"` does **not** opt
a stepper out of an ancestor's rung — to reset the calc-driven scale use
`style="--md-sys-density-scale: 0"` (the ancestor's `--md-sys-spacing-*`
payload still inherits).

**i18n** — every user-facing word is a prop: `label`, `step-word`, `of-word`,
`completed-word`, `current-word`, `error-word`, `optional-word`, `next-label`,
`back-label`, `finish-label`, plus each step's `label`, `description` and
`error-text`. The assembled order ("Step 2 of 4: Payment, current") is fixed,
so locales needing a different order should adjust the wording to fit, or
override a step's whole announced name with its `accessible-name`.

## Related components

`md-step` · `md-tabs` · `md-accordion` · `md-progress-indicator` ·
`md-breadcrumbs` · `md-button`

## Theming

| Custom property | Purpose | Default |
|---|---|---|
| `--md-stepper-gap` | Gap between steps (vertical only) | `0` |
| `--md-step-connector-color` | Pending mobile dot (and every step's connector track) | `--md-sys-color-outline` here, `--md-sys-color-outline-variant` in `md-step` |
| `--md-step-active-color` | Active / done mobile dot (and every step's active bubble) | `--md-sys-color-primary` |

`--md-stepper-gap` defaults to `0` on purpose: the vertical connector rail is
continuous only while adjacent step boxes touch, so any other value opens gaps
in it. Add padding inside the step content instead.

**CSS parts** — `list`, `content`, `nav`, `mobile-nav`, `mobile-progress`.

```css
md-stepper.brand {
  --md-step-active-color: var(--md-sys-color-tertiary);
  --md-step-connector-color: var(--md-sys-color-outline-variant);
}
md-stepper.brand::part(nav) {
  justify-content: space-between;
}
```

Everything else about a step's appearance is themed with the `--md-step-*`
properties documented in the `md-step` readme.

<!-- Auto Generated Below -->


## Overview

`md-stepper` — Material Design 3 stepper. Orchestrates a set of slotted
`md-step` children: it owns the active index and navigation, and pushes
layout / i18n down to each step.

**Controlled, with optional auto-progress.** The stepper owns `active`
(updated on click / `next` / `prev` / `goTo`, emitting `mdStepChange`). With
`auto-complete` (default), advancing marks the step you leave as `completed`,
so the progress line + checks "just work" — no manual wiring. Set
`auto-complete="false"` to own `completed` yourself.

```html
<md-stepper active="0">
  <md-step label="Account"><p>…</p></md-step>
  <md-step label="Shipping" optional><p>…</p></md-step>
  <md-step label="Payment" editable><p>…</p></md-step>
</md-stepper>
```

## Properties

| Property        | Attribute        | Description                                                                                                                                                                                                                                                                                                                                                                          | Type                         | Default        |
| --------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------- | -------------- |
| `active`        | `active`         | Index of the active step. Two-way bindable. Out-of-range values clamp.                                                                                                                                                                                                                                                                                                               | `number`                     | `0`            |
| `autoComplete`  | `auto-complete`  | When advancing (via `next()` or a step's built-in Continue button), mark the step being left as `completed`. Default `true`. Turn off to fully control completion yourself.                                                                                                                                                                                                          | `boolean`                    | `true`         |
| `backLabel`     | `back-label`     | Label for the built-in Back button.                                                                                                                                                                                                                                                                                                                                                  | `string`                     | `'Back'`       |
| `completedWord` | `completed-word` | Localized word announced for a completed step ("…, completed").                                                                                                                                                                                                                                                                                                                      | `string`                     | `'completed'`  |
| `currentWord`   | `current-word`   | Localized word announced for the current step ("…, current").                                                                                                                                                                                                                                                                                                                        | `string`                     | `'current'`    |
| `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`            |
| `errorWord`     | `error-word`     | Localized word announced for a step in error ("…, error: message").                                                                                                                                                                                                                                                                                                                  | `string`                     | `'error'`      |
| `finishLabel`   | `finish-label`   | Label for the built-in Continue button on the last step.                                                                                                                                                                                                                                                                                                                             | `string`                     | `'Finish'`     |
| `indicator`     | `indicator`      | Indicator style: `numbered` circles or minimal `dot`s.                                                                                                                                                                                                                                                                                                                               | `"dot" \| "numbered"`        | `'numbered'`   |
| `label`         | `label`          | Accessible name for the navigation landmark.                                                                                                                                                                                                                                                                                                                                         | `string`                     | `'Progress'`   |
| `lazy`          | `lazy`           | Only mount the **active** vertical step's content panel; inactive panels are removed from layout and the a11y tree (no collapse animation). Use for wizards with heavy per-step content. Note: slotted custom elements stay *connected* in the light DOM — for full teardown, conditionally render the content in your framework.                                                    | `boolean`                    | `false`        |
| `loading`       | `loading`        | Put the built-in **Continue / Finish** button into a loading state (spinner + interactive-disabled) and disable **Back**, for async submit-on-Continue (prevents double-submit). Toggle it around your `await` — typically inside an `mdBeforeChange` handler that `preventDefault()`s, does async work, then commits by setting `active`.                                           | `boolean`                    | `false`        |
| `mode`          | `mode`           | - `linear` (default) — steps must be completed in order; future steps are   not reachable until the prior ones are done or optional (an `editable`   completed step can always be revisited). - `non-linear` — any step can be selected.                                                                                                                                             | `"linear" \| "non-linear"`   | `'linear'`     |
| `nav`           | `nav`            | Show the built-in **Back / Continue** navigation (default `true`) so users always have a clear way to move through the flow: - vertical → the buttons sit under each active step (Material wizard style), - horizontal → a single button bar below the stepper. Set `nav="false"` to drive navigation entirely yourself (clicking steps, `next()` / `prev()`, or your own controls). | `boolean`                    | `true`         |
| `nextDisabled`  | `next-disabled`  | Disable the built-in **Continue / Finish** button without disabling Back or the whole step. Bind this to your current step's form validity to gate advancement declaratively (the most common wizard need). Step clicks and the `next()`/`goTo()` methods are unaffected — use `mode="linear"` and/or `mdBeforeChange` for those.                                                    | `boolean`                    | `false`        |
| `nextLabel`     | `next-label`     | Label for the built-in Continue button.                                                                                                                                                                                                                                                                                                                                              | `string`                     | `'Continue'`   |
| `ofWord`        | `of-word`        | Localized word for "of" in each step's announced name ("Step 2 of 4").                                                                                                                                                                                                                                                                                                               | `string`                     | `'of'`         |
| `optionalWord`  | `optional-word`  | Caption + announced word for optional steps.                                                                                                                                                                                                                                                                                                                                         | `string`                     | `'Optional'`   |
| `orientation`   | `orientation`    | Layout orientation.                                                                                                                                                                                                                                                                                                                                                                  | `"horizontal" \| "vertical"` | `'horizontal'` |
| `stepWord`      | `step-word`      | Localized word for "Step" in each step's announced name ("Step 2 of 4").                                                                                                                                                                                                                                                                                                             | `string`                     | `'Step'`       |
| `variant`       | `variant`        | Layout variant: - `default` — the full step row (headers + connectors). - `mobile` — a compact MUI-style bar for narrow screens: **Back**, a centered   progress (dots for `indicator="dot"`, else a "Step N of M" caption), and   **Continue / Finish**. The step headers are hidden; put the active step's   panel in the `content` slot and swap it on `mdStepChange`.            | `"default" \| "mobile"`      | `'default'`    |


## Events

| Event            | Description                                                                                                                                                                                                                                                                                                                                                                                                                                          | Type                                                |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
| `mdBeforeChange` | Fires BEFORE a user-driven step change commits (step click, built-in nav, or `next()`/`prev()`/`goTo()`). Cancelable: call `event.preventDefault()` to veto the transition (e.g. block Continue until the current step validates). `detail.index` is the requested step, `detail.previous` the current one. Does not fire for a direct `active` property assignment — set `active` yourself to commit authoritatively (e.g. after async validation). | `CustomEvent<{ index: number; previous: number; }>` |
| `mdComplete`     | Continue pressed on the last step (the flow is finished).                                                                                                                                                                                                                                                                                                                                                                                            | `CustomEvent<void>`                                 |
| `mdStepChange`   | Active step changed (after it commits).                                                                                                                                                                                                                                                                                                                                                                                                              | `CustomEvent<{ index: number; previous: number; }>` |


## Methods

### `goTo(index: number) => Promise<void>`

Jump to a step (out-of-range clamps; honors `linear` reachability; emits
`mdBeforeChange`).

#### Parameters

| Name    | Type     | Description |
| ------- | -------- | ----------- |
| `index` | `number` |             |

#### Returns

Type: `Promise<void>`



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

Advance to the next non-disabled step (emitting a cancelable
`mdBeforeChange`). When `auto-complete` is on, the step you leave is marked
completed; past the last reachable step it emits `mdComplete`.

#### Returns

Type: `Promise<void>`



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

Step backward to the previous non-disabled step (emitting `mdBeforeChange`).

#### Returns

Type: `Promise<void>`



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

Reset to the first step and clear completed / error state.

#### Returns

Type: `Promise<void>`




## Slots

| Slot        | Description                                                                                                                                                                                                                                                 |
| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|             | `md-step` children.                                                                                                                                                                                                                                         |
| `"content"` | Content area for **horizontal** steppers, rendered between           the step row and the built-in nav bar. Put the active step's           panel here and swap it on `mdStepChange` (vertical steppers           render content inside each step instead). |


## Shadow Parts

| Part                | Description                                               |
| ------------------- | --------------------------------------------------------- |
| `"content"`         | Wrapper around the `content` slot.                        |
| `"list"`            | The ordered list wrapping the steps.                      |
| `"mobile-nav"`      | The compact `variant="mobile"` bar.                       |
| `"mobile-progress"` | The centered progress (dots / caption) in the mobile bar. |
| `"nav"`             | The horizontal Back / Continue button bar.                |


## Dependencies

### Depends on

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

### Graph
```mermaid
graph TD;
  md-stepper --> md-button
  md-button --> md-ripple
  md-button --> md-loading-indicator
  style md-stepper 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.