Skip to content

Time Picker

Choose a time, by dial or by typing. 12h/24h display, min/max bounds with templated validation messages, and full constraint validation through ElementInternals.

Live preview — click a field to open the picker 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-time-picker label="Start time" value="09:30"></md-time-picker>
<md-time-picker label="24-hour" format="24h" value="17:45"></md-time-picker>
<md-time-picker label="Dial first" variant="dial" value="12:15"></md-time-picker>

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


<md-time-picker></md-time-picker>
  • Any time-of-day entry: appointments, reminders, opening hours.
  • Where a coarse selection is typical (minute-step="5" / "15").
SituationUse instead
A datemd-date-picker
A duration (“90 minutes”)md-text-field type="number" / md-select
A time rangeTwo pickers with min/max wired to each other
A few fixed slotsmd-select / md-chip
A timestamp the user shouldn’t editFormatted text
Timezone selectionmd-select
NeedSetting
Typed entryvariant="input"
A clock facevariant="dial"
Wide layout for landscapevariant="dial-landscape"
Pick by viewportvariant="responsive"
12- or 24-hourformat
Coarser minutesminute-step
Restrict the rangemin / max
No built-in triggerhide-trigger + show()
NeedSetting
Keyboard entry first (default)variant="input"
Clock dial firstvariant="dial"
Adapt to viewportresponsive
Landscape dialorientation="horizontal"
AM/PM side by sideperiod-layout="horizontal"

Prefer variant="input" as the default: typing is faster and more accessible than dragging a dial for most users. Either way both modes stay reachable — the dialog carries a toggle in its footer.

Input, dial, landscape dial, responsive 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-time-picker label="Input variant" variant="input" value="08:00"></md-time-picker>
<md-time-picker label="Dial variant" variant="dial" value="08:00"></md-time-picker>
<md-time-picker label="Dial, landscape" variant="dial" orientation="horizontal" value="08:00"></md-time-picker>
<md-time-picker label="Responsive" responsive value="08:00"></md-time-picker>

format="12h" is the default and shows an AM/PM toggle; format="24h" drops it. Set it from the user’s locale convention — do not hardcode 12h.

Formats — all three hold the same 14:30 value 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-time-picker label="12-hour (default)" format="12h" value="14:30"></md-time-picker>
<md-time-picker label="24-hour" format="24h" value="14:30"></md-time-picker>
<md-time-picker label="12h, horizontal AM/PM" format="12h" period-layout="horizontal" value="14:30"></md-time-picker>

minute-step snaps the dial and the entry field to real granularity. Offering minute precision for hour-long slots is needless precision.

Minute step 1, 5 and 15 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-time-picker label="Every minute (default)" variant="dial" value="10:07"></md-time-picker>
<md-time-picker label="Every 5 minutes" variant="dial" minute-step="5" value="10:05"></md-time-picker>
<md-time-picker label="Every 15 minutes" variant="dial" minute-step="15" value="10:15"></md-time-picker>

min/max bound the range, required demands a value, and the messages are templates: {min} and {max} interpolate. Keep those tokens when translating or the message loses the values it exists to state.

PropMessage shown when
value-missing-labelrequired and nothing chosen
range-underflow-labelBefore min
range-overflow-labelAfter max
range-outside-labelOutside a wrapping range (min later than max)
Bounded pickers with templated messages 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-time-picker
  label="Office hours"
  name="start"
  format="24h"
  min="09:00"
  max="17:00"
  minute-step="15"
  required
  value="09:30"
  range-underflow-label="Please choose a time at or after {min}."
  range-overflow-label="Please choose a time at or before {max}."
  value-missing-label="Please select a time."
  ></md-time-picker>

  <md-time-picker label="Overnight shift" format="24h" min="22:00" max="06:00" value="23:00"
    range-outside-label="Please choose a time at or after {min} or at or before {max}."></md-time-picker>
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-time-picker label="Enabled" value="09:00"></md-time-picker>
<md-time-picker label="Disabled" value="09:00" disabled></md-time-picker>
<md-time-picker label="Required, empty" required></md-time-picker>

hide-trigger renders the picker without its own field, for embedding in your own layout. You call show() — and hide() to close it, not close().

hide-trigger — your own button drives show() Open in Storybook
Pick a time no time chosen
Show code for each technology
<!-- index.html <head> — the icon font the components draw from -->
<link rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">

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

<md-button id="tp-open" variant="filled" icon="schedule">Pick a time</md-button>
<md-time-picker id="tp" hide-trigger format="24h" minute-step="5"></md-time-picker>

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

  document.getElementById('tp-open')
    .addEventListener('mdClick', () => picker.show());

  picker.addEventListener('mdChange', () => console.log(picker.value));
  // close it with hide(), not close()
</script>
EventCancelableDetailFires
mdChangenoMdTimePickerChangeDetailA time is committed
mdInputnoMdTimePickerChangeDetailDuring entry — do not wire a save to this
mdOpen / mdClosenovoidThe dialog opens / closes
mdCancelnovoidThe dialog is dismissed
mdModeChangenodial ⇄ inputThe footer mode toggle is used
mdValidityChangeno{ valid, validationMessage, flags }Validity is recomputed
mdChange, mdCancel and mdValidityChange as they fire
Open the picker, commit a time, then try one outside 09:00–17:00.
Show code for each technology
<md-time-picker id="start" label="Start time" name="start"
format="24h" min="09:00" max="17:00" minute-step="15" required
range-underflow-label="Please choose a time at or after {min}."
range-overflow-label="Please choose a time at or before {max}."
></md-time-picker>

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

t.addEventListener('mdChange', (e) => save(e.detail));   // value stays "HH:MM"
t.addEventListener('mdCancel', () => console.log('dismissed'));
t.addEventListener('mdValidityChange', (e) => {
  console.log(e.detail.valid, e.detail.validationMessage);
});
</script>

There is no range variant. Two pickers make one by feeding each other’s bounds: committing the From time becomes the To picker’s min, and vice versa, so the pair can never end up inverted. Set a time in either one and watch the other’s bounds tighten.

Two coupled pickers form a range
From 09:00 to 17:00 — no bounds applied yet.
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-time-picker id="from" label="From" format="24h" minute-step="15" value="09:00"></md-time-picker>
<md-time-picker id="to" label="To" format="24h" minute-step="15" value="17:00"></md-time-picker>

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

  // Each committed value becomes the other picker's bound, so the pair can
  // never invert. mdChange fires on commit — mdInput would fight the user
  // mid-edit.
  from.addEventListener('mdChange', () => { to.min = from.value; });
  to.addEventListener('mdChange', () => { from.max = to.value; });
</script>

Use mdChange, not mdInput: bounds applied mid-edit would fight the user as they type. Both pickers keep their own validation — a value outside the bound it was given reports rangeUnderflow / rangeOverflow with the localized range-underflow-label / range-overflow-label message.

Properties

Property Attribute Type Default Reflects
variant variant MdTimePickerMode 'input' —
format format MdTimePickerFormat '12h' —
value value string '' Yes
name name string '' Yes
min min string '' —
max max string '' —
required required boolean false Yes
label label string 'Select time' —
supportingText supporting-text string '' —
error error boolean false Yes
errorText error-text string '' —
reserveSupportingSpace reserve-supporting-space boolean false —
headline headline string '' —
headlineInputLabel headline-input-label string 'Enter time' —
headlineDialLabel headline-dial-label string 'Select time' —
valueMissingLabel value-missing-label string 'Please select a time.' —
rangeUnderflowLabel range-underflow-label string 'Please select a time at or after {min}.' —
rangeOverflowLabel range-overflow-label string 'Please select a time at or before {max}.' —
rangeOutsideLabel range-outside-label string 'Please select a time at or after {min} or at or before {max}.' —
disabled disabled boolean false Yes
open open boolean false Yes
minuteStep minute-step number 1 —
cancelLabel cancel-label string 'Cancel' —
okLabel ok-label string 'OK' —
hideTrigger hide-trigger boolean false Yes
amLabel am-label string 'AM' —
pmLabel pm-label string 'PM' —
hourLabel hour-label string 'Hour' —
minuteLabel minute-label string 'Minute' —
toggleDialLabel toggle-dial-label string 'Toggle dial picker' —
toggleInputLabel toggle-input-label string 'Toggle keyboard input' —
periodLabel period-label string 'Period' —
periodLayout period-layout MdTimePickerPeriodLayout 'vertical' —
orientation orientation MdTimePickerOrientation 'vertical' —
responsive responsive boolean false —
density density 0 | -1 | -2 | -3 | -4 0 Yes

Methods

Method Parameters
show() none
hide() none
checkValidity() none
reportValidity() none
getValidity() none

CSS Custom Properties

Override on the host element for per-instance theming:

Property Description
--md-time-picker-trigger-color Trigger label/value color
--md-time-picker-trigger-outline-color Trigger outline color
--md-time-picker-trigger-hover-color Trigger leading-icon color on hover
--md-time-picker-trigger-focus-color Trigger focus indicator color
--md-time-picker-trigger-icon-color Trigger leading icon color
--md-time-picker-dialog-color Dialog surface color
--md-time-picker-dialog-on-color Dialog text color
--md-time-picker-scrim-color Modal scrim color (rgba)
--md-time-picker-headline-color Headline label color
--md-time-picker-period-bg Period (AM/PM) container bg
--md-time-picker-period-color Period (AM/PM) text color
--md-time-picker-period-active-bg Period selected bg
--md-time-picker-period-active-color Period selected color
--md-time-picker-period-outline-color Period outline color
--md-time-picker-period-hover-opacity Period state-layer opacity on hover (e.g. 8% / 12%)
--md-time-picker-dial-bg Dial surface color
--md-time-picker-dial-number-color Dial number text color
--md-time-picker-dial-number-active-color Selected number color
--md-time-picker-dial-hand-color Hand / centre-dot color
--md-time-picker-input-bg HH/MM tile bg (inactive)
--md-time-picker-input-color HH/MM tile text color (inactive)
--md-time-picker-input-active-bg HH/MM tile bg when focused (default = primary-container)
--md-time-picker-input-active-color HH/MM tile text when focused (default = on-primary-container)
--md-time-picker-input-outline-color HH/MM tile resting outline color (default transparent)
--md-time-picker-input-error-color Error outline / hint color
--md-time-picker-input-hint-color "Hour" / "Minute" helper-label color
--md-time-picker-input-separator-color Colon glyph between HH and MM
--md-time-picker-input-hover-opacity HH/MM state-layer opacity (e.g. 8%)
--md-time-picker-action-color Cancel label color
--md-time-picker-action-primary-color OK label color
--md-time-picker-action-cancel-bg Cancel button container color (default transparent — set to apply a tonal/filled style)
--md-time-picker-action-confirm-bg OK button container color (default transparent — set to apply a tonal/filled style)
--md-time-picker-toggle-icon-color Mode-toggle icon color
--md-time-picker-toggle-bg Mode-toggle container color (default transparent)
--md-time-picker-trigger-shape Trigger corner-radius (piped onto md-text-field)
--md-time-picker-dialog-shape Dialog corner-radius
--md-time-picker-input-shape HH/MM tile corner-radius (canonical name; --md-time-picker-time-display-shape kept as back-compat alias)
--md-time-picker-period-shape AM/PM container corner-radius
--md-time-picker-action-shape Cancel / OK button corner-radius (piped onto md-button)
--md-time-picker-toggle-shape Mode-toggle corner-radius (piped onto md-icon-button)
--md-time-picker-trigger-min-width Preferred intrinsic trigger width (default 240px)
--md-time-picker-trigger-icon-size Trigger leading icon font-size (default 24px, tapers with density)
--md-time-picker-dialog-padding Dialog internal padding (default 24px)
--md-time-picker-dialog-min-width Dialog width in portrait / input variants (default 328px)
--md-time-picker-dialog-horizontal-width-12h Horizontal dial dialog width in 12h mode (default 572px)
--md-time-picker-dialog-horizontal-width-24h Horizontal dial dialog width in 24h mode (default 608px)
--md-time-picker-input-width HH/MM tile inline-size (default 96px = 4/3 of the height; derived from the font unless set)
--md-time-picker-input-height HH/MM tile block-size (default 72px = 1.6 x the font; derived from the font unless set)
--md-time-picker-input-border-width HH/MM tile border width (default 2px)
--md-time-picker-input-separator-width Colon column width (default 24px)
--md-time-picker-input-hint-gap Gap between tile and "Hour" / "Minute" hint (default 4px)
--md-time-picker-period-outline-width AM/PM container border width (default 1px)
--md-time-picker-period-vertical-width AM/PM vertical-stack width (default 52px)
--md-time-picker-period-vertical-height AM/PM vertical-stack height in dial variant (default 80px)
--md-time-picker-period-vertical-height-input AM/PM vertical-stack height in input variant (default 72px)
--md-time-picker-period-horizontal-width AM/PM horizontal-pill width (default 216px)
--md-time-picker-period-horizontal-height AM/PM horizontal-pill height (default 38px)
--md-time-picker-dial-size Dial face diameter (default 256px)
--md-time-picker-dial-target-size Per-number touch-target diameter (default 48px)
--md-time-picker-dial-center-size Centre-dot diameter (default 8px)
--md-time-picker-dial-hand-width Hand-line stroke width (default 2px)
--md-time-picker-hand-length Hand-line length override (default: computed — 100%, ~62% on the 24h inner ring)
--md-time-picker-footer-gap Gap between mode-toggle and actions (default 8px)
--md-time-picker-actions-gap Gap between Cancel and OK (default 8px)
--md-time-picker-toggle-icon-size Mode-toggle icon font-size (default 24px, tapers with density)
--md-time-picker-headline-spacing Margin below headline (default 20px)
--md-time-picker-headline-font-size Headline font-size (default label-medium = 12px)
--md-time-picker-headline-font-weight Headline font-weight (default 500)
--md-time-picker-input-font-size HH/MM tile font-size (default display-medium = 45px)
--md-time-picker-input-font-weight HH/MM tile font-weight (default 400)
--md-time-picker-period-font-size AM/PM label font-size (default 14px)
--md-time-picker-period-font-weight AM/PM label font-weight (default 500)
--md-time-picker-dial-number-font-size Dial number font-size (default body-large = 16px)
--md-time-picker-dial-number-font-weight Dial number resting font-weight (default 400)
--md-time-picker-dial-number-active-font-weight Dial number active font-weight (default 500)
--md-time-picker-dialog-enter-duration Dialog open animation duration (default 500ms)
--md-time-picker-mode-swap-duration Dial⇄input swap animation duration (default 450ms)
--md-time-picker-motion-easing Easing curve shared by both entry animations (default emphasized-decelerate)
--md-time-picker-dialog-elevation Dialog box-shadow
--md-time-picker-time-display-shape —
--md-time-picker-input-active-outline-color —

CSS Shadow Parts

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

Part Description
period-am AM button
period-pm PM button
trigger Composed md-text-field that opens the dialog
trigger-icon Schedule icon slotted into trigger's leading slot
period-toggle AM/PM toggle group
period-am-ripple —
period-pm-ripple —
dial-wrap Wrapper around the dial (dial variant only)
dial Dial surface (drag target; dial variant only)
dial-hand Selection hand (dial variant only)
input-area Container for the typeable HH/MM input row
input-hour HH text input tile
input-minute MM text input tile
time-area Dial-variant wrapper around input-area + the
scrim Modal scrim
dialog Dialog surface
headline Headline label ("Enter time" / "Select time")
body —
footer Footer with mode toggle + actions
toggle-mode Icon button switching dial<->input
actions Action button row (Cancel / OK)
cancel-button Cancel button
confirm-button OK (confirm) button
  • The dial and the input fields are both keyboard operable. hour-label, minute-label and period-label are the accessible names of the entry fields and the AM/PM control.
  • toggle-dial-label / toggle-input-label name the mode switch — translate them.
  • The dialog traps focus and returns it to the trigger on close.
  • Validation messages are announced. The templated range messages are far more useful than a generic “invalid” because they state the actual bound.
  • Prefer variant="input" as the default for the reasons above.

RTL — the dialog, field order and AM/PM toggle mirror under dir="rtl". The clock dial itself keeps clockwise numbering — that is not a directional cue. See RTL.

Density — density="-1…-4" compacts the trigger field. The dialog is deliberately unaffected: measured across the rungs, the dial keeps a 327px diameter and its numbers keep 48px hit targets, so a compact form can never produce a dial you cannot hit. See Density.

Trigger field — density 0 through -4 (56 → 40px)
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: flex; gap: 16px; align-items: flex-start; flex-wrap: wrap;">
  <md-time-picker label="Density 0" format="24h" value="09:00" density="0"></md-time-picker>
  <md-time-picker label="-1" format="24h" value="09:00" density="-1"></md-time-picker>
  <md-time-picker label="-2" format="24h" value="09:00" density="-2"></md-time-picker>
  <md-time-picker label="-3" format="24h" value="09:00" density="-3"></md-time-picker>
  <md-time-picker label="-4" format="24h" value="09:00" density="-4"></md-time-picker>
</div>

i18n — every visible string is a prop, all defaulting to English: label, headline, headline-input-label, headline-dial-label, am-label, pm-label, hour-label, minute-label, period-label, cancel-label, ok-label, toggle-dial-label, toggle-input-label, and the four validation messages. Keep {min} / {max} intact in the range messages, and set format from the locale’s convention.

Fully localized (fr) 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-time-picker
  label="Heure de début"
  format="24h"
  value="09:30"
  headline-input-label="Saisir l'heure"
  headline-dial-label="Sélectionner l'heure"
  hour-label="Heure"
  minute-label="Minute"
  cancel-label="Annuler"
  ok-label="Valider"
  toggle-dial-label="Basculer vers le cadran"
  toggle-input-label="Basculer vers la saisie"
  value-missing-label="Veuillez sélectionner une heure."
  ></md-time-picker>
Custom propertyPurpose
--md-time-picker-trigger-color / -outline-color / -hover-color / -focus-colorTrigger field
--md-time-picker-trigger-icon-colorTrigger glyph
--md-time-picker-dialog-color / -dialog-on-colorDialog surface
--md-time-picker-scrim-colorBackdrop
--md-time-picker-headline-colorHeadline
--md-time-picker-period-bg / -period-active-bgAM/PM toggle
--md-time-picker-dial-bg / -dial-hand-colorClock dial
--md-time-picker-input-bg / -input-active-bgEntry fields
Themed instances — open one to see the dialog tokens 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-time-picker label="Tertiary accent" value="11:20"
  style="--md-time-picker-dial-hand-color: var(--md-sys-color-tertiary); --md-time-picker-headline-color: var(--md-sys-color-tertiary); --md-time-picker-input-active-bg: var(--md-sys-color-tertiary-container);"></md-time-picker>
  <md-time-picker label="Squared trigger" value="11:20"
    style="--md-time-picker-trigger-shape: 6px; --md-time-picker-dialog-shape: 8px;"></md-time-picker>

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

PartElement
trigger / trigger-iconThe field trigger and its glyph
scrim / dialogModal backdrop and panel
headline / body / footerDialog regions
dial / dial-hand / dial-wrapThe clock face, its hand, its wrapper
input-area / input-hour / input-minuteTyped-entry region and its two fields
time-areaThe hour–minute cluster
period-toggle / period-am / period-pmAM/PM control and its halves
toggle-modeThe dial ⇄ input switch
actions / cancel-button / confirm-buttonThe action row
trigger-icon, dial-hand, headline and confirm-button — open it 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>

<style>
  .parts-tp::part(trigger-icon) { color: var(--md-sys-color-primary); }
  .parts-tp::part(dial-hand) { opacity: .9; }
  .parts-tp::part(headline) { font-style: italic; }
  .parts-tp::part(confirm-button) { font-weight: 700; }
</style>
<md-time-picker class="parts-tp" label="Styled parts" value="14:30" variant="dial" style="min-inline-size: 240px;"></md-time-picker>
md-time-picker::part(dial-hand) {
opacity: 0.9;
}

md-date-picker · md-text-field · md-select · md-dialog · md-icon-button · md-button

For AI Agents — md-time-picker

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-time-picker 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-time-picker readme.md

# md-time-picker

<!-- llm:meta
tag: md-time-picker
category: pickers
status: md3-mapped
m3-guidelines: https://m3.material.io/components/time-pickers/guidelines
form-associated: true
depends-on: md-text-field, md-ripple, md-icon-button, md-button
used-by: none
-->

**Choose a time, by dial or by typing.** 12h/24h formats, min/max bounds with
templated validation messages, overnight ranges, and full constraint validation.

> Setup, theming, density and i18n are configured once for the whole library —
> see [`main-llm.md`](../../../../../main-llm.md), the library-wide specification shipped
> alongside these manuals.

---

## When to use

- Any **time-of-day** entry: appointments, reminders, opening hours.
- Where a coarse selection is typical (`minute-step="5"` / `"15"`).
- Where a form must submit a `HH:MM` value like a native `<input type="time">`.

## When NOT to use

| Situation | Use instead |
|---|---|
| A date | `md-date-picker` |
| A duration ("90 minutes") | `md-text-field type="number"` / `md-select` |
| A time range | Two pickers with `min`/`max` wired |
| A few fixed slots | `md-select` / `md-chip` |
| A timestamp the user shouldn't edit | Formatted text |
| Timezone selection | `md-select` |

## Decision cues

| Need | Setting |
|---|---|
| Keyboard entry first | `variant="input"` (default) |
| Clock dial first | `variant="dial"` |
| 12-hour with AM/PM | `format="12h"` (default) |
| 24-hour | `format="24h"` |
| Coarse minutes on the dial | `minute-step="5"` / `"15"` |
| Bound the range | `min` / `max` (`HH:MM`) |
| Overnight window (22:00 → 06:00) | `min="22:00" max="06:00"` — reversed is supported |
| Adapt dial/input to the viewport | `responsive` |
| Landscape dial | `orientation="horizontal"` |
| AM/PM side by side | `period-layout="horizontal"` |
| Embed without the built-in trigger | `hide-trigger` + `show()` |

## API contract

```html
<md-time-picker
  variant="input|dial"                 <!-- default: input -->
  format="12h|24h"                     <!-- default: 12h -->
  value="14:30"
  name="start"
  min="09:00"
  max="17:00"
  required
  disabled
  open
  minute-step="5"                      <!-- default: 1 -->
  period-layout="vertical|horizontal"  <!-- default: vertical -->
  orientation="vertical|horizontal"    <!-- default: vertical; dial variant only -->
  responsive                           <!-- default: false -->
  hide-trigger                         <!-- default: false -->
  density="-1|-2|-3|-4"                <!-- omit for the default rung -->
></md-time-picker>
```

Every visible string is a prop, each defaulting to English:

```html
<md-time-picker
  label="Select time"
  headline=""                          <!-- overrides both headline-*-label -->
  headline-input-label="Enter time"
  headline-dial-label="Select time"
  am-label="AM"
  pm-label="PM"
  hour-label="Hour"
  minute-label="Minute"
  period-label="Period"
  cancel-label="Cancel"
  ok-label="OK"
  toggle-dial-label="Toggle dial picker"
  toggle-input-label="Toggle keyboard input"
  value-missing-label="Please select a time."
  range-underflow-label="Please select a time at or after {min}."
  range-overflow-label="Please select a time at or before {max}."
  range-outside-label="Please select a time at or after {min} or at or before {max}."
></md-time-picker>
```

**Events** — `mdChange` (committed) and `mdInput` (every intermediate change)
both carry the same `MdTimePickerChangeDetail`; `mdOpen` / `mdClose` /
`mdCancel` carry no detail; `mdModeChange` carries `{ mode, previousMode }`;
`mdValidityChange` is `bubbles: true`, **`composed: false`** — listen on the
element or a light-DOM ancestor, never across a shadow boundary.

`MdTimePickerChangeDetail` gives you four representations of one time so you
never re-format it yourself:

```js
{
  value: '14:30',                          // canonical, matches the `value` prop
  iso: '14:30:00',                         // HH:MM:SS local time
  isoDateTime: '2026-05-21T14:30:00.000Z', // today's date, serialized to UTC
  date: Date,                              // today's date with these H/M, s+ms zeroed
  hours: 14,                               // 0–23
  minutes: 30,                             // 0–59
  period: 'PM'                             // derived from hours, always present
}
```

**Methods** — `show()`, `hide()`, `getValidity()`, `checkValidity()`,
`reportValidity()`. All are async; `await` them.

**Slots** — none. The component renders no `<slot>`; every string is a prop, so
localization and layout stay under the component's control. Restyle through the
CSS parts.

**Parts** — `trigger`, `trigger-icon`, `scrim`, `dialog`, `headline`, `body`,
`time-area`, `input-area`, `input-hour`, `input-minute`, `period-toggle`,
`period-am`, `period-am-ripple`, `period-pm`, `period-pm-ripple`, `dial-wrap`,
`dial`, `dial-hand`, `footer`, `toggle-mode`, `actions`, `cancel-button`,
`confirm-button`.

### Behavioral contract worth knowing

- **`value` is a 24-hour `HH:MM` string regardless of `format`.** `format="12h"`
  only changes the display; a consumer parsing `value` never handles AM/PM.
  `min` and `max` use the same form.
- **`value` is canonicalized on load.** `"9:05"` becomes `"09:05"`, so the
  reflected attribute and `FormData` never change shape later. A malformed
  `value` is rejected and treated as empty (with a dev-build console warning).
- **`mdChange` only fires on commit** — the OK button, or Enter in the HH/MM
  fields while both buffers are valid. `mdInput` fires on every intermediate
  change: dial drag, dial keyboard entry, the AM/PM toggle, and input-variant
  keystrokes that produce a valid `HH:MM`.
- **Cancel, `Escape` and a scrim click all emit `mdCancel`** and restore the
  previously committed time. `hide()` is treated as a Cancel and emits it too.
  `mdClose` fires on every close, commit or cancel alike.
- **Close it with `hide()`, not `close()`** — this is the one picker in the
  family using `hide()`. (`md-date-picker` uses `close()`.)
- **A reversed `min`/`max` is an overnight window**, matching native
  `<input type="time">`: `min="22:00" max="06:00"` accepts 23:30 and 05:00. A
  value in the excluded middle sets **both** `rangeUnderflow` and
  `rangeOverflow` and reports `range-outside-label`.
- The three range messages are **templates**: `{min}` and `{max}` interpolate,
  every occurrence. **Keep those tokens when translating.**
- `minute-step` snaps the **dial** only. The HH/MM fields always accept
  1-minute granularity.
- `orientation` applies to `variant="dial"` only; the input dialog is always the
  vertical ~328px layout. `period-layout` is ignored when `format="24h"`, since
  there is no AM/PM toggle to lay out. A horizontal dial dialog always uses the
  horizontal AM/PM pill regardless of `period-layout`.
- `responsive` treats `variant` / `orientation` as a *preference*: it falls back
  to the input variant below ~460px of viewport height, and promotes the dial to
  horizontal at ≥720px wide in landscape (demoting again below ~620px, so the
  two thresholds do not oscillate). It only overrides when the viewport cannot
  fit what you asked for.
- `hide-trigger` renders no field at all and makes the host `display: contents`
  — open it yourself with `show()`.
- **Form-associated via `ElementInternals`.** With a `name` it submits the
  committed `HH:MM` under that name (an empty selection submits an empty entry,
  like a native time input). Form reset restores the `value` the element had at
  first render, not empty. A wrapping `<fieldset disabled>` disables it through
  `formDisabledCallback` and closes an open dialog.
- `mdValidityChange` never fires on mount — the first evaluation only primes the
  baseline — and never re-fires for a state that did not change.
- **Users can switch modes from inside the open dialog** via the footer toggle,
  whatever `variant` you set. `mdModeChange` reports the switch; `variant`
  itself is not mutated.
- The trigger is a read-only `md-text-field`: Tab focuses it, click and
  Enter/Space open the dialog, and free text cannot be typed into it.
- The picker keeps an intrinsic trigger width by default. Set the host's
  `inline-size` (for example `100%` in a form) to stretch the whole trigger;
  it also shrinks with a narrower host without a `::part(trigger)` override.
  `--md-time-picker-trigger-min-width` supplies the preferred intrinsic size.

---

## Do / Don't

Sourced from [M3 · Time pickers · Guidelines](https://m3.material.io/components/time-pickers/guidelines).

| ✅ Do | ❌ Don't |
|---|---|
| Offer keyboard entry for a known time | Don't force dial interaction for a time the user can type |
| Use the dial for approximate or exploratory selection | Don't use a dial where precision to the minute matters |
| Match `format` to the user's locale convention | Don't force 12h on a 24h locale (or the reverse) |
| Use `minute-step` matching real granularity | Don't offer minute precision for hour-long slots |
| Bound with `min` / `max` | Don't accept an out-of-hours time then reject it |
| Keep `{min}` / `{max}` when translating range messages | Don't drop the tokens — the message loses the values |
| Localize every label prop | Don't ship the English defaults |
| Show validation inline | Don't rely on the browser balloon |
| Turn on `responsive` on new integrations | Don't pin `orientation="horizontal"` on a phone-portrait layout |

---

## Patterns

```html
<!-- Bounded, validated, submitted with a form -->
<form id="shift">
  <md-time-picker
    id="start" label="Start time" name="start"
    format="24h" min="09:00" max="17:00" minute-step="15" required
    range-underflow-label="Please choose a time at or after {min}."
    range-overflow-label="Please choose a time at or before {max}."
  ></md-time-picker>
  <md-button type="submit">Save</md-button>
</form>

<script type="module">
  const t = document.getElementById('start');
  t.addEventListener('mdChange', (e) => console.log(e.detail.value));  // "09:30"
  t.addEventListener('mdCancel', () => console.log('dismissed'));

  document.getElementById('shift').addEventListener('submit', (e) => {
    e.preventDefault();
    console.log(new FormData(e.currentTarget).get('start'));  // "09:30"
  });
</script>
```

```html
<!-- Dial first, adaptive layout -->
<md-time-picker variant="dial" orientation="horizontal" responsive></md-time-picker>
```

```html
<!-- A full-width form control; the field follows the host's width. -->
<md-time-picker label="Start time" style="inline-size: 100%" responsive></md-time-picker>
```

```html
<!-- A range, as two coupled pickers -->
<md-time-picker id="from" label="From"></md-time-picker>
<md-time-picker id="to"   label="To"></md-time-picker>

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

  // mdChange, not mdInput: bounds applied mid-edit fight the user as they type.
  from.addEventListener('mdChange', () => { to.min = from.value; });
  to.addEventListener('mdChange', () => { from.max = to.value; });
</script>
```

```html
<!-- Embedded, no built-in trigger; close with hide(), not close() -->
<md-button id="open-tp">Pick a time</md-button>
<md-time-picker hide-trigger id="tp"></md-time-picker>

<script type="module">
  const tp = document.getElementById('tp');
  document.getElementById('open-tp').addEventListener('mdClick', () => tp.show());
</script>
```

```html
<!-- Overnight window: valid at or after 22:00 OR at or before 06:00 -->
<md-time-picker
  label="Night shift start" format="24h"
  min="22:00" max="06:00"
  range-outside-label="Night shifts start between {min} and {max}."
></md-time-picker>
```

### Ranges

There is no range variant. Two pickers make one by feeding each other's bounds
(see the pattern above). Each keeps its own constraint validation, so a value
past the bound it was given reports `rangeUnderflow` / `rangeOverflow` with the
localized message.

## Anti-patterns

| ❌ Wrong | ✅ Right | Why |
|---|---|---|
| Coupling a range on `mdInput` | Couple on `mdChange` | Bounds applied mid-edit fight the user as they type. |
| Expecting `value` to be `"2:30 PM"` with `format="12h"` | It is always `"14:30"` | `format` affects display only. |
| `picker.close()` | `picker.hide()` | This component uses `hide()`, unlike `md-date-picker`. |
| Translating range messages without `{min}` / `{max}` | Keep the tokens | The bounds interpolate into them. |
| Wiring both `mdInput` and `mdChange` to a save | Use `mdChange` | `mdInput` fires on every intermediate change. |
| `minute-step="1"` for 30-minute slots | Match the real granularity | Needless precision on the dial. |
| Expecting `minute-step` to constrain the HH/MM fields | It snaps the dial only | Typed entry is always 1-minute. |
| Hardcoding `format="12h"` for all locales | Set it per locale | Wrong convention for most of the world. |
| Setting `period-layout` with `format="24h"` | Drop it | There is no AM/PM toggle in 24h mode. |
| Shipping the English label props | Translate all of them | Half-localized dialog otherwise. |
| Listening for `mdValidityChange` on a shadow ancestor | Listen on the element itself | It is `composed: false`. |
| A hidden `<input>` to submit the time | Give the picker a `name` | It is already form-associated. |
| `md-time-picker` for a duration | `md-text-field` / `md-select` | Different concept — a duration has no clock face. |
| Slotting custom content into the dialog | Use the label props and CSS parts | The component renders no `<slot>`. |

## Accessibility, RTL, density, i18n

**Accessibility** — the dial and the HH/MM fields are both keyboard operable.
`hour-label`, `minute-label` and `period-label` are the accessible names of the
entry fields and of the AM/PM `radiogroup`, which also implements the WAI-ARIA
radio keyboard pattern (arrows, Home, End) so PM is reachable without Tab.
`toggle-dial-label` / `toggle-input-label` name the mode switch — translate
them. The dialog is `role="dialog" aria-modal="true"`, labelled by its headline;
it traps Tab, and Escape is handled by the topmost open picker only, so stacked
pickers close one at a time. Focus returns to the trigger on close. The trigger
carries `aria-haspopup="dialog"` and a live `aria-expanded`. Validation messages
are anchored on the trigger field; the templated range messages are far more
useful than a generic "invalid" because they state the bound.

**RTL** — the dialog, the field order and the AM/PM toggle mirror under
`dir="rtl"`. The clock dial itself keeps clockwise numbering — that is not
directional.

**Density** — `density="-1…-4"` locally overrides the inherited `data-density`
rung; only those four rungs exist, and omitting the attribute is the uncompacted
default. `density="0"` does **not** opt a picker out of an ancestor's rung — for
that, set `style="--md-sys-density-scale: 0"`. The trigger icon, headline, HH/MM
tiles, AM/PM label and dial numerals all taper; the **dial face keeps its 256px
diameter and its 48px hit targets at every rung**, so a compact form never
produces a dial you cannot hit.

**i18n** — translate every label prop listed in the API contract, keep
`{min}` / `{max}` in the range messages, and set `format` from the locale's
convention. The picker does no `Intl` formatting of its own: `value` is always
canonical `HH:MM`.

## Related components

`md-date-picker` · `md-text-field` · `md-select` · `md-dialog` ·
`md-icon-button` · `md-button`

## Theming

| Custom property | Purpose | Default |
|---|---|---|
| `--md-time-picker-trigger-color` | Trigger field text | `--md-sys-color-on-surface` |
| `--md-time-picker-trigger-outline-color` | Trigger field outline | `--md-sys-color-outline` |
| `--md-time-picker-trigger-hover-color` | Trigger hover text | `--md-sys-color-on-surface` |
| `--md-time-picker-trigger-focus-color` | Trigger focused outline / label | `--md-sys-color-primary` |
| `--md-time-picker-trigger-icon-color` | Trigger clock glyph | `--md-sys-color-on-surface-variant` |
| `--md-time-picker-trigger-icon-size` | Trigger glyph size | 24px, tapering 1px per density rung (18px floor) |
| `--md-time-picker-trigger-shape` | Trigger corner radius | `--md-sys-shape-corner-extra-small` (4px) |
| `--md-time-picker-trigger-min-width` | Preferred intrinsic trigger width; an explicit host width can shrink it | `240px` |
| `--md-time-picker-dialog-color` | Dialog surface | `--md-sys-color-surface-container-high` |
| `--md-time-picker-dialog-on-color` | Dialog foreground | `--md-sys-color-on-surface` |
| `--md-time-picker-dialog-shape` | Dialog corner radius | `--md-sys-shape-corner-extra-large` (28px) |
| `--md-time-picker-dialog-elevation` | Dialog shadow | `--md-sys-elevation-3` |
| `--md-time-picker-dialog-padding` | Dialog inset | `--md-sys-spacing-inset-xl` (24px) |
| `--md-time-picker-dialog-min-width` | Vertical dialog width | `328px` |
| `--md-time-picker-dialog-horizontal-width-12h` | Landscape dialog width, 12h | `572px` |
| `--md-time-picker-dialog-horizontal-width-24h` | Landscape dialog width, 24h | `608px` |
| `--md-time-picker-scrim-color` | Backdrop | `rgba(0, 0, 0, 0.32)` |
| `--md-time-picker-headline-color` | Headline text | `--md-sys-color-on-surface-variant` |
| `--md-time-picker-headline-font-size` | Headline size | 12px, tapering 0.5px per rung (10px floor) |
| `--md-time-picker-headline-font-weight` | Headline weight | `500` |
| `--md-time-picker-headline-spacing` | Headline to body gap | 20px, tapering 2px per rung (4px floor) |
| `--md-time-picker-input-bg` | HH/MM tile background | `--md-sys-color-surface-container-highest` |
| `--md-time-picker-input-color` | HH/MM tile digits | `--md-sys-color-on-surface` |
| `--md-time-picker-input-active-bg` | Selected tile background | `--md-sys-color-primary-container` |
| `--md-time-picker-input-active-color` | Selected tile digits | `--md-sys-color-on-primary-container` |
| `--md-time-picker-input-outline-color` | Tile outline | `transparent` |
| `--md-time-picker-input-active-outline-color` | Selected tile outline | `--md-sys-color-primary` |
| `--md-time-picker-input-error-color` | Tile error outline / text | `--md-sys-color-error` |
| `--md-time-picker-input-hint-color` | "Hour" / "Minute" hint text | `--md-sys-color-on-surface-variant` |
| `--md-time-picker-input-shape` | Tile corner radius | `--md-sys-shape-corner-small` (8px) |
| `--md-time-picker-input-font-size` | Tile digit size | 45px, tapering 2px per rung (32px floor) |
| `--md-time-picker-input-height` / `-input-width` | Tile box | Derived from the digit size (72 × 96 at rung 0) |
| `--md-time-picker-input-border-width` | Tile outline width | `2px` |
| `--md-time-picker-input-separator-color` / `-separator-width` | The ":" between tiles | Dialog foreground / `24px` |
| `--md-time-picker-period-bg` / `-period-color` | AM/PM resting | `transparent` / `--md-sys-color-on-surface` |
| `--md-time-picker-period-active-bg` / `-period-active-color` | AM/PM selected | `--md-sys-color-tertiary-container` / `--md-sys-color-on-tertiary-container` |
| `--md-time-picker-period-outline-color` / `-period-outline-width` | AM/PM outline | `--md-sys-color-outline` / `1px` |
| `--md-time-picker-period-shape` | AM/PM corner radius | `--md-sys-shape-corner-small` (8px) |
| `--md-time-picker-period-vertical-width` / `-vertical-height` | Vertical AM/PM pill | `52px` / `80px` (72px in the input variant) |
| `--md-time-picker-period-horizontal-width` / `-horizontal-height` | Horizontal AM/PM pill | `216px` / `38px` |
| `--md-time-picker-dial-bg` | Dial face | `--md-sys-color-surface-container-highest` |
| `--md-time-picker-dial-size` | Dial diameter (does **not** taper) | `256px` |
| `--md-time-picker-dial-target-size` | Numeral hit target | `48px` |
| `--md-time-picker-dial-hand-color` / `-hand-width` | Selection hand | `--md-sys-color-primary` / `2px` |
| `--md-time-picker-dial-center-size` | Hand pivot dot | `8px` |
| `--md-time-picker-dial-number-color` / `-number-active-color` | Dial numerals | `--md-sys-color-on-surface` / `--md-sys-color-on-primary` |
| `--md-time-picker-dial-number-font-size` | Dial numeral size | 16px, tapering 0.5px per rung (13px floor) |
| `--md-time-picker-action-color` / `-action-primary-color` | Cancel / OK labels | `--md-sys-color-primary` |
| `--md-time-picker-action-cancel-bg` / `-action-confirm-bg` | Action backgrounds | `transparent` |
| `--md-time-picker-action-shape` | Action corner radius | `--md-sys-shape-corner-full` |
| `--md-time-picker-footer-gap` / `-actions-gap` | Footer spacing | `--md-sys-spacing-gap-sm` (8px) |
| `--md-time-picker-toggle-bg` / `-toggle-shape` | Mode-toggle button | `transparent` / `--md-sys-shape-corner-full` |
| `--md-time-picker-toggle-icon-color` / `-toggle-icon-size` | Mode-toggle glyph | `--md-sys-color-on-surface-variant` / 24px, tapering 1px per rung |
| `--md-time-picker-dialog-enter-duration` | Dialog entrance | `--md-sys-motion-duration-long2` (500ms) |
| `--md-time-picker-mode-swap-duration` | Dial ⇄ input swap | `--md-sys-motion-duration-long1` (450ms) |
| `--md-time-picker-motion-easing` | Both of the above | `--md-sys-motion-easing-emphasized-decelerate` |

**CSS parts** — see the **Parts** list in the API contract; the full generated
list is in **Shadow Parts** below.

```css
md-time-picker.brand {
  --md-time-picker-dial-hand-color: var(--md-sys-color-tertiary);
  --md-time-picker-input-active-bg: var(--md-sys-color-tertiary-container);
  --md-time-picker-input-active-color: var(--md-sys-color-on-tertiary-container);
}
```

<!-- Auto Generated Below -->


## Properties

| Property              | Attribute               | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           | Type                         | Default                                                           |
| --------------------- | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | ----------------------------------------------------------------- |
| `amLabel`             | `am-label`              | Localized AM label (12-hour mode).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    | `string`                     | `'AM'`                                                            |
| `cancelLabel`         | `cancel-label`          | Localized label for the Cancel action.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | `string`                     | `'Cancel'`                                                        |
| `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`                                                               |
| `disabled`            | `disabled`              | Disables the trigger and prevents the dialog from opening.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | `boolean`                    | `false`                                                           |
| `format`              | `format`                | 12-hour (with AM/PM) or 24-hour clock.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | `"12h" \| "24h"`             | `'12h'`                                                           |
| `headline`            | `headline`              | Headline copy shown above the dial / inputs. Overrides the mode-specific defaults (`headline-input-label` / `headline-dial-label`) when set.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | `string`                     | `''`                                                              |
| `headlineDialLabel`   | `headline-dial-label`   | Localized default headline for the dial variant (used when `headline` is empty).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `string`                     | `'Select time'`                                                   |
| `headlineInputLabel`  | `headline-input-label`  | Localized default headline for the input variant (used when `headline` is empty).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | `string`                     | `'Enter time'`                                                    |
| `hideTrigger`         | `hide-trigger`          | When true the built-in field trigger is not rendered; open the dialog programmatically via `.show()`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | `boolean`                    | `false`                                                           |
| `hourLabel`           | `hour-label`            | Localized accessible label for the Hour input field. Matches the [MD3 Time picker accessibility spec](https://m3.material.io/components/time-pickers/accessibility) which mandates an "Hour" label on the hour text input.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | `string`                     | `'Hour'`                                                          |
| `label`               | `label`                 | Trigger field label (acts as the floating label of the text-field-shaped trigger).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    | `string`                     | `'Select time'`                                                   |
| `max`                 | `max`                   | Latest selectable time as a 24-hour `HH:MM` string. A committed value after it fails constraint validation with `rangeOverflow`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `string`                     | `''`                                                              |
| `min`                 | `min`                   | Earliest selectable time as a 24-hour `HH:MM` string. A committed value before it fails constraint validation with `rangeUnderflow`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | `string`                     | `''`                                                              |
| `minuteLabel`         | `minute-label`          | Localized accessible label for the Minute input field. Matches the [MD3 Time picker accessibility spec](https://m3.material.io/components/time-pickers/accessibility) which mandates a "Minute" label on the minute text input.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       | `string`                     | `'Minute'`                                                        |
| `minuteStep`          | `minute-step`           | Minute snap increment on the dial (1, 5, 10, 15, 30…). Inputs always accept 1-minute granularity.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | `number`                     | `1`                                                               |
| `name`                | `name`                  | Form field name — the picker is form-associated and submits its `value` under this name in `FormData`, like a native `<input type="time">`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           | `string`                     | `''`                                                              |
| `okLabel`             | `ok-label`              | Localized label for the confirm action.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | `string`                     | `'OK'`                                                            |
| `open`                | `open`                  | Whether the picker dialog is open. Two-way bindable / reflected.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `boolean`                    | `false`                                                           |
| `orientation`         | `orientation`           | Dialog orientation for the dial variant.  - `vertical` (default) — single-column portrait layout (~328px wide). - `horizontal` — two-column landscape layout (~572px wide in both   12h and 24h modes since the HH/MM input tiles are 96px in either   format). Forces the AM/PM pill into the 216×38px horizontal   variant since the 52×72px vertical pill cannot share the left   column cleanly. Has no effect when `variant="input"`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | `"horizontal" \| "vertical"` | `'vertical'`                                                      |
| `periodLabel`         | `period-label`          | Localized accessible label for the AM/PM radio group container. Wraps the two radio children; individual radio names come from `am-label` / `pm-label`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | `string`                     | `'Period'`                                                        |
| `periodLayout`        | `period-layout`         | Layout for the AM/PM period selector. Has no effect when `format="24h"` (the toggle is not rendered).  - `vertical` — 52×72px stack next to the minute tile (default). - `horizontal` — 216×38px segmented button below HH:MM.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | `"horizontal" \| "vertical"` | `'vertical'`                                                      |
| `pmLabel`             | `pm-label`              | Localized PM label (12-hour mode).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    | `string`                     | `'PM'`                                                            |
| `rangeOutsideLabel`   | `range-outside-label`   | Localized validation message for a reversed (overnight) `min`/`max` window when the time falls in the excluded middle. `{min}` and `{max}` are substituted.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           | `string`                     | `'Please select a time at or after {min} or at or before {max}.'` |
| `rangeOverflowLabel`  | `range-overflow-label`  | Localized validation message when the committed time is after `max`. `{max}` is substituted.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | `string`                     | `'Please select a time at or before {max}.'`                      |
| `rangeUnderflowLabel` | `range-underflow-label` | Localized validation message when the committed time is before `min`. `{min}` is substituted.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `string`                     | `'Please select a time at or after {min}.'`                       |
| `required`            | `required`              | Requires a committed value for the host form to validate (`valueMissing` otherwise).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | `boolean`                    | `false`                                                           |
| `responsive`          | `responsive`            | Adaptive layout policy. Per the MD3 spec the picker should swap orientation or variant based on the viewport so the dial never has to scroll. When `responsive` is enabled the dialog automatically:    • Falls back to `variant="input"` when the viewport height is     too short to fit the 256px dial (default threshold: 460px),     matching the spec line "Time pickers can fallback to the     input time picker when there isn't enough vertical real     estate to present the landscape orientation without     scrolling".   • Promotes the dial variant to `orientation="horizontal"`     when the viewport is wide enough for the 572/608px landscape     dialog (default threshold: width ≥ 720px AND width > height),     matching "the time picker can change to landscape orientation     on larger breakpoints or when viewport height is limited".  The `variant` / `orientation` props remain the *preference* the picker tries to honor — adaptive overrides only kick in when the viewport literally cannot fit them. Default `false` for backward compatibility; flip to `true` on new integrations to get the canonical MD3 adaptive layout. | `boolean`                    | `false`                                                           |
| `toggleDialLabel`     | `toggle-dial-label`     | Localized accessible label for the mode-toggle icon button when the picker is currently in the *input* variant (clock icon shown — click to switch to the dial). Matches the MD3 accessibility spec entry "Clock button → Toggle dial picker".                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | `string`                     | `'Toggle dial picker'`                                            |
| `toggleInputLabel`    | `toggle-input-label`    | Localized accessible label for the mode-toggle icon button when the picker is currently in the *dial* variant (keyboard icon shown — click to switch back to the input). Symmetric extension of the MD3 "Toggle dial picker" naming pattern, since the spec only documents the input-mode label explicitly.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           | `string`                     | `'Toggle keyboard input'`                                         |
| `value`               | `value`                 | Selected time as a 24-hour `HH:MM` string (e.g. `"14:30"`).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           | `string`                     | `''`                                                              |
| `valueMissingLabel`   | `value-missing-label`   | Localized constraint-validation message when `required` and no time is committed.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | `string`                     | `'Please select a time.'`                                         |
| `variant`             | `variant`               | Picker mode shown when the dialog first opens. Defaults to `input` (keyboard-first); users can toggle to `dial` from the footer icon.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | `"dial" \| "input"`          | `'input'`                                                         |


## Events

| Event              | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | Type                                                                                          |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------- |
| `mdCancel`         | Emitted when the picker is cancelled or dismissed without committing.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    | `CustomEvent<void>`                                                                           |
| `mdChange`         | Emitted when the user confirms the chosen time (OK button, or Enter inside the HH/MM fields while the buffers are valid). This is the canonical "the user has committed a value" event — use it for form integration, persistence, and analytics.  The detail bundle includes both the human-friendly `value` and three industry-standard ISO / Date representations so downstream code never has to re-format the time itself.                                                                                                                                          | `CustomEvent<MdTimePickerChangeDetail>`                                                       |
| `mdClose`          | Emitted when the picker closes (whether confirmed, cancelled, or dismissed).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | `CustomEvent<void>`                                                                           |
| `mdInput`          | Emitted on every intermediate change while the dialog is open — dial drag, dial-mode keyboard typing, AM/PM toggle, and input-variant keystrokes that produce a valid HH:MM.  Mirrors the HTML `input` event semantics ("fires for every user-driven mutation") and matches the pattern used by `md-date-picker` / `md-text-field`. Use `mdInput` for live previews and `mdChange` for the final commit. The detail shape is identical to `mdChange` so a previewer can read the same fields regardless of which event arrived.                                          | `CustomEvent<MdTimePickerChangeDetail>`                                                       |
| `mdModeChange`     | Emitted when the user switches between the dial and input variants via the dialog's keyboard / clock toggle. Lets analytics or sibling UI react to the variant change (e.g. announce the new mode to screen readers, or resize a hosting dialog wrapper).                                                                                                                                                                                                                                                                                                                | `CustomEvent<MdTimePickerModeChangeDetail>`                                                   |
| `mdOpen`           | Emitted when the picker opens.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           | `CustomEvent<void>`                                                                           |
| `mdValidityChange` | Fires when this control's validity CHANGES — never on every keystroke, and never for a re-publish that lands on the same state.  `composed: false` is deliberate. Composites like md-select embed an md-text-field, and a composed event escapes that inner shadow root, so a listener on md-select would receive the inner field's event as well as the host's — two events, different payloads, for one logical control. Keeping it uncomposed means each component reports only for itself, while `bubbles: true` still lets a <form> or app root hear every control. | `CustomEvent<{ valid: boolean; validationMessage: string; flags: Record<string, boolean>; }>` |


## Methods

### `checkValidity() => Promise<boolean>`

Constraint-validation parity with native form controls: true when
the committed value satisfies `required` / `min` / `max`.

#### Returns

Type: `Promise<boolean>`



### `getValidity() => Promise<{ valid: boolean; validationMessage: string; } & Partial<ValidityState>>`

Snapshot of the host's ValidityState + message (ElementInternals).

#### Returns

Type: `Promise<{ valid: boolean; validationMessage: string; } & Partial<ValidityState>>`



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

Programmatically close the picker dialog (treated as Cancel).

#### Returns

Type: `Promise<void>`



### `reportValidity() => Promise<boolean>`

Like `checkValidity()` but also surfaces the browser's validation
UI anchored on the trigger field (matches md-text-field).

#### Returns

Type: `Promise<boolean>`



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



#### Returns

Type: `Promise<void>`




## Shadow Parts

| Part                 | Description |
| -------------------- | ----------- |
| `"actions"`          |             |
| `"body"`             |             |
| `"cancel-button"`    |             |
| `"confirm-button"`   |             |
| `"dial"`             |             |
| `"dial-hand"`        |             |
| `"dial-wrap"`        |             |
| `"dialog"`           |             |
| `"footer"`           |             |
| `"headline"`         |             |
| `"input-area"`       |             |
| `"input-hour"`       |             |
| `"input-minute"`     |             |
| `"period-am"`        |             |
| `"period-am-ripple"` |             |
| `"period-pm"`        |             |
| `"period-pm-ripple"` |             |
| `"period-toggle"`    |             |
| `"scrim"`            |             |
| `"time-area"`        |             |
| `"toggle-mode"`      |             |
| `"trigger"`          |             |
| `"trigger-icon"`     |             |


## Dependencies

### Depends on

- [md-text-field](../md-text-field)
- [md-ripple](../md-ripple)
- [md-icon-button](../md-icon-button)
- [md-button](../md-button)

### Graph
```mermaid
graph TD;
  md-time-picker --> md-text-field
  md-time-picker --> md-ripple
  md-time-picker --> md-icon-button
  md-time-picker --> md-button
  md-icon-button --> md-ripple
  md-button --> md-ripple
  md-button --> md-loading-indicator
  style md-time-picker 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: 56
sub-component-count: 25
manual-count: 81
per-component-docs: ./packages/core/src/components/<tag>/readme.md
-->

**Use this reference when building, updating, or reviewing an interface with
AWC UI, a Material Design 3 web-component library.** Start with the user's
request and the existing project. The sections below cover setup, component
selection, composition, and verification; read the sections relevant to the
task. Per-component detail lives in
`./packages/core/src/components/<tag>/readme.md` — e.g.
[`md-button`](./packages/core/src/components/md-button/readme.md).

1. **Match the task's scope** (§1). Reuse decisions from the project and
   conversation. A focused edit does not need a product interview or scaffold.
2. **Choose documented components** using the decision matrix (§5), and read
   each affected component's manual before changing its markup (§6). Check its
   API, `When NOT to use`, accessibility, and `Anti-patterns` sections; do not
   infer behavior from a similar library or sibling component.
3. **Use the existing integration.** For a new app or a requested setup change,
   use §2–§4. For composition, consult §7 and the relevant recipes in §8.
4. **Follow the API and accessibility rules** (§9). Do not invent an `md-*`
   tag, prop, event, slot, CSS part, or token. If the documented API cannot
   satisfy the request, explain the gap and resolve the consequential choice.
5. **Verify the affected behavior** using the applicable checks in §10.
   Report what changed and what was checked. A review reports findings without
   changing files unless a fix was requested.

---

## §1 — Match the task

| Task | How to proceed |
|---|---|
| New app | Establish the app's purpose and use known project or user decisions. Ask only about missing choices that materially affect the result; use reasonable defaults for minor details and state consequential assumptions. Then apply the relevant setup in §2–§4. |
| Existing app or focused fix | Preserve the framework, configuration, design, and product scope unless the request changes them. Inspect the affected code and manuals, then make the requested change. Do not restart discovery or scaffold another app. |
| Review or explanation | Inspect and explain the requested surface. Keep the work read-only unless the user asks for a fix. |

### Optional discovery checklist

Use the questions below as a reference when a new app or requested feature
leaves an important decision open. They are not a required interview, an
ordered sequence, or a reason to stop a focused task. Project configuration
and answers already given in the conversation take precedence; do not ask for
them again. Ask only what matters to the current task, and group related
questions when that helps. The **bold** options are starting defaults for a
new app, not instructions to override an existing project.

### 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,
    the feature needs a third-party editor (TipTap, Lexical, Quill) styled with
    MD3 tokens. Resolve that choice if the project or request does not cover it.

### 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

Apply this reference to new setup or requested configuration changes. Preserve
an existing project's choices for unrelated work.

| 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 20.3.31+ |
| Vue / Nuxt | `@awc-ui/vue` | Vue 3 |
| Svelte / SvelteKit | `@awc-ui/svelte` | Svelte 5.57+ |
| 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.
  Invalid submissions show the platform's message in the control's own inline
  error styling, with no browser validation popover. Correcting or resetting a
  field clears its generated message. `error` + `error-text` remain available
  for app-provided messages and take precedence over generated messages.
  `checkValidity()` checks silently; `reportValidity()` shows inline errors and
  focuses the first invalid control. Core submit buttons and Enter do the same.
- 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.

The library has 56 components and 25 sub-components, with 81 manuals in total.
Sub-components 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 Reusable interaction patterns

- **One footer for a dialog wizard.** Set `md-stepper.nav = false` and put
  Back / Continue in the dialog's `actions` slot. Keep the steps in the body;
  the dialog owns scrolling and keeps its header/footer visible. In fullscreen
  mode use the header close button for dismissal. Use a real form and submit
  button so Enter and inline validation follow the same path as a click.
- **Overlay handoffs wait for completion.** Use `await search.close(); await
  search.whenClosed();` before opening a result dialog or unmounting search.
  `mdClose` signals the state change; `whenClosed()` includes motion and cleanup.
  Do not replace that contract with a guessed timeout or a second focus trap.
- **Appearance controls fill their allocated space.** For an inline color picker
  in settings, set `--md-color-picker-width: 100%`. Its compact default remains
  appropriate for a popover. No shadow-DOM width overrides or extra tabindex.
- **Loading belongs to the operation.** Use `md-button.loading` for the initiating
  action and `md-skeleton` with `announce=false` for the content being replaced.
  Preserve the content layout, expose one live status, and guard repeated submits.
  Abort or ignore stale responses when the owner closes or a newer request wins.
  Provide inline error/retry and preserve entered values; do not add artificial waits.
- **Separate routing intent from selection synchronization.** `md-navigation-bar`
  emits `mdChange` for both user and programmatic changes. For a controlled router,
  use each `md-navigation-tab`'s `mdTabClick` for user intent, `manual-activation`
  on the bar, and synchronize `activeIndex` from the confirmed route. Avoid routing
  again merely because browser Back/Forward updated the selected index.
- **Keep table chrome outside the column scroller.** Use `slot="top"` and
  `slot="bottom"` on the toolbar and pagination. Set `min-inline-size: 0` on the
  containing grid/flex child and a readable minimum width on the table. Nested
  controls own their keys; do not add row-level Enter/Space interception in apps.

### 7.4 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

Use the checks relevant to the affected components and behavior. For a small
fix, verify the changed interaction and nearby regressions; a new app or broad
change needs wider coverage. Do not turn a focused edit into an unrelated
full-app audit.

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.


## MCP server and reusable skills

Core provides a read-only MCP server (`@awc-ui/mcp`) and `awc-ui-build` / `awc-ui-review` skills. MCP tools `search_components`, `get_component`, `list_guides`, and `get_guide` expose versioned Core documentation. Match the reported Core version to the consumer installation; use local manuals when versions differ. The server is optional and grants no permission for project mutations.

The Core package ships skills in `skills/`; `awc-ui ai-setup --skills` installs them under the consumer project’s `.agents/skills/` without overwriting customized copies. For configuration and skill-installer availability, see [AI integration](https://awc-ui.dev/guides/building-with-ai/#optional-mcp-and-skills). Use existing project and user decisions rather than restarting discovery for a focused edit.