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

PropertyAttributeTypeDefaultReflects
variantvariantMdTimePickerMode'input'
formatformatMdTimePickerFormat'12h'
valuevaluestring''Yes
namenamestring''Yes
minminstring''
maxmaxstring''
requiredrequiredbooleanfalseYes
labellabelstring'Select time'
headlineheadlinestring''
headlineInputLabelheadline-input-labelstring'Enter time'
headlineDialLabelheadline-dial-labelstring'Select time'
valueMissingLabelvalue-missing-labelstring'Please select a time.'
rangeUnderflowLabelrange-underflow-labelstring'Please select a time at or after {min}.'
rangeOverflowLabelrange-overflow-labelstring'Please select a time at or before {max}.'
rangeOutsideLabelrange-outside-labelstring'Please select a time at or after {min} or at or before {max}.'
disableddisabledbooleanfalseYes
openopenbooleanfalseYes
minuteStepminute-stepnumber1
cancelLabelcancel-labelstring'Cancel'
okLabelok-labelstring'OK'
hideTriggerhide-triggerbooleanfalseYes
amLabelam-labelstring'AM'
pmLabelpm-labelstring'PM'
hourLabelhour-labelstring'Hour'
minuteLabelminute-labelstring'Minute'
toggleDialLabeltoggle-dial-labelstring'Toggle dial picker'
toggleInputLabeltoggle-input-labelstring'Toggle keyboard input'
periodLabelperiod-labelstring'Period'
periodLayoutperiod-layoutMdTimePickerPeriodLayout'vertical'
orientationorientationMdTimePickerOrientation'vertical'
responsiveresponsivebooleanfalse
densitydensity0 | -1 | -2 | -3 | -40Yes

Methods

MethodParameters
show()none
hide()none
checkValidity()none
reportValidity()none
getValidity()none

CSS Custom Properties

Override on the host element for per-instance theming:

PropertyDescription
--md-time-picker-trigger-colorTrigger label/value color
--md-time-picker-trigger-outline-colorTrigger outline color
--md-time-picker-trigger-hover-colorTrigger leading-icon color on hover
--md-time-picker-trigger-focus-colorTrigger focus indicator color
--md-time-picker-trigger-icon-colorTrigger leading icon color
--md-time-picker-dialog-colorDialog surface color
--md-time-picker-dialog-on-colorDialog text color
--md-time-picker-scrim-colorModal scrim color (rgba)
--md-time-picker-headline-colorHeadline label color
--md-time-picker-period-bgPeriod (AM/PM) container bg
--md-time-picker-period-colorPeriod (AM/PM) text color
--md-time-picker-period-active-bgPeriod selected bg
--md-time-picker-period-active-colorPeriod selected color
--md-time-picker-period-outline-colorPeriod outline color
--md-time-picker-period-hover-opacityPeriod state-layer opacity on hover (e.g. 8% / 12%)
--md-time-picker-dial-bgDial surface color
--md-time-picker-dial-number-colorDial number text color
--md-time-picker-dial-number-active-colorSelected number color
--md-time-picker-dial-hand-colorHand / centre-dot color
--md-time-picker-input-bgHH/MM tile bg (inactive)
--md-time-picker-input-colorHH/MM tile text color (inactive)
--md-time-picker-input-active-bgHH/MM tile bg when focused (default = primary-container)
--md-time-picker-input-active-colorHH/MM tile text when focused (default = on-primary-container)
--md-time-picker-input-outline-colorHH/MM tile resting outline color (default transparent)
--md-time-picker-input-error-colorError outline / hint color
--md-time-picker-input-hint-color"Hour" / "Minute" helper-label color
--md-time-picker-input-separator-colorColon glyph between HH and MM
--md-time-picker-input-hover-opacityHH/MM state-layer opacity (e.g. 8%)
--md-time-picker-action-colorCancel label color
--md-time-picker-action-primary-colorOK label color
--md-time-picker-action-cancel-bgCancel button container color (default transparent — set to apply a tonal/filled style)
--md-time-picker-action-confirm-bgOK button container color (default transparent — set to apply a tonal/filled style)
--md-time-picker-toggle-icon-colorMode-toggle icon color
--md-time-picker-toggle-bgMode-toggle container color (default transparent)
--md-time-picker-trigger-shapeTrigger corner-radius (piped onto md-text-field)
--md-time-picker-dialog-shapeDialog corner-radius
--md-time-picker-input-shapeHH/MM tile corner-radius (canonical name; --md-time-picker-time-display-shape kept as back-compat alias)
--md-time-picker-period-shapeAM/PM container corner-radius
--md-time-picker-action-shapeCancel / OK button corner-radius (piped onto md-button)
--md-time-picker-toggle-shapeMode-toggle corner-radius (piped onto md-icon-button)
--md-time-picker-trigger-min-widthTrigger min-inline-size (default 240px)
--md-time-picker-trigger-icon-sizeTrigger leading icon font-size (default 24px, tapers with density)
--md-time-picker-dialog-paddingDialog internal padding (default 24px)
--md-time-picker-dialog-min-widthDialog width in portrait / input variants (default 328px)
--md-time-picker-dialog-horizontal-width-12hHorizontal dial dialog width in 12h mode (default 572px)
--md-time-picker-dialog-horizontal-width-24hHorizontal dial dialog width in 24h mode (default 608px)
--md-time-picker-input-widthHH/MM tile inline-size (default 96px = 4/3 of the height; derived from the font unless set)
--md-time-picker-input-heightHH/MM tile block-size (default 72px = 1.6 x the font; derived from the font unless set)
--md-time-picker-input-border-widthHH/MM tile border width (default 2px)
--md-time-picker-input-separator-widthColon column width (default 24px)
--md-time-picker-input-hint-gapGap between tile and "Hour" / "Minute" hint (default 4px)
--md-time-picker-period-outline-widthAM/PM container border width (default 1px)
--md-time-picker-period-vertical-widthAM/PM vertical-stack width (default 52px)
--md-time-picker-period-vertical-heightAM/PM vertical-stack height in dial variant (default 80px)
--md-time-picker-period-vertical-height-inputAM/PM vertical-stack height in input variant (default 72px)
--md-time-picker-period-horizontal-widthAM/PM horizontal-pill width (default 216px)
--md-time-picker-period-horizontal-heightAM/PM horizontal-pill height (default 38px)
--md-time-picker-dial-sizeDial face diameter (default 256px)
--md-time-picker-dial-target-sizePer-number touch-target diameter (default 48px)
--md-time-picker-dial-center-sizeCentre-dot diameter (default 8px)
--md-time-picker-dial-hand-widthHand-line stroke width (default 2px)
--md-time-picker-hand-lengthHand-line length override (default: computed — 100%, ~62% on the 24h inner ring)
--md-time-picker-footer-gapGap between mode-toggle and actions (default 8px)
--md-time-picker-actions-gapGap between Cancel and OK (default 8px)
--md-time-picker-toggle-icon-sizeMode-toggle icon font-size (default 24px, tapers with density)
--md-time-picker-headline-spacingMargin below headline (default 20px)
--md-time-picker-headline-font-sizeHeadline font-size (default label-medium = 12px)
--md-time-picker-headline-font-weightHeadline font-weight (default 500)
--md-time-picker-input-font-sizeHH/MM tile font-size (default display-medium = 45px)
--md-time-picker-input-font-weightHH/MM tile font-weight (default 400)
--md-time-picker-period-font-sizeAM/PM label font-size (default 14px)
--md-time-picker-period-font-weightAM/PM label font-weight (default 500)
--md-time-picker-dial-number-font-sizeDial number font-size (default body-large = 16px)
--md-time-picker-dial-number-font-weightDial number resting font-weight (default 400)
--md-time-picker-dial-number-active-font-weightDial number active font-weight (default 500)
--md-time-picker-dialog-enter-durationDialog open animation duration (default 500ms)
--md-time-picker-mode-swap-durationDial⇄input swap animation duration (default 450ms)
--md-time-picker-motion-easingEasing curve shared by both entry animations (default emphasized-decelerate)
--md-time-picker-dialog-elevationDialog 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():

PartDescription
period-amAM button
period-pmPM button
triggerComposed md-text-field that opens the dialog
trigger-iconSchedule icon slotted into trigger's leading slot
period-toggleAM/PM toggle group
period-am-ripple
period-pm-ripple
dial-wrapWrapper around the dial (dial variant only)
dialDial surface (drag target; dial variant only)
dial-handSelection hand (dial variant only)
input-areaContainer for the typeable HH/MM input row
input-hourHH text input tile
input-minuteMM text input tile
time-areaDial-variant wrapper around input-area + the
scrimModal scrim
dialogDialog surface
headlineHeadline label ("Enter time" / "Select time")
body
footerFooter with mode toggle + actions
toggle-modeIcon button switching dial<->input
actionsAction button row (Cancel / OK)
cancel-buttonCancel button
confirm-buttonOK (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.

Densitydensity="-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>

i18nevery 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.

---

## 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 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` | Trigger min inline size | `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: 57
sub-component-count: 24
manual-count: 81
per-component-docs: ./packages/core/src/components/<tag>/readme.md
-->

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

Your job, in order:

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

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

---

## §1 — Interview the user

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

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

### 1.1 Scope and shape

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

### 1.2 Look and feel

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

### 1.3 Internationalization

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

### 1.4 Data and forms

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

### 1.5 Constraints

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

---

## §2 — Map answers to configuration

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

### 2.1 Global switches — the complete set

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

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

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

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

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

---

## §3 — Install and bootstrap

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

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

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

defineCustomElements(window);
```

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

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

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

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

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

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

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

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

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

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

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

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

---

## §4 — Global configuration reference

### 4.1 The token system

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

#### Color roles

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

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

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

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

#### Shape tokens

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

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

#### Elevation tokens

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

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

#### Motion tokens

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

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

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

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

#### State-layer opacities

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

#### Typography, spacing and layering

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

### 4.2 Theming and rebranding

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

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

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

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

### 4.3 Density

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

Two details that bite:

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

### 4.4 Dark mode

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

### 4.5 RTL

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

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

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

### 4.6 Internationalization

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

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

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

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

### 4.7 Forms and validation

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

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

Rules:

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

---

## §5 — Component decision matrix

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

### 5.1 Actions

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

### 5.2 Text input

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

### 5.3 Selection

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

### 5.4 Navigation

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

### 5.5 Containment and feedback

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

### 5.6 Data

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

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

### 5.7 Choosing the variant

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

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

### 5.8 Not in the library

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

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

---

## §6 — Component inventory

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

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

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

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

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

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

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

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

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

---

## §7 — Composition rules

### 7.1 What nests inside what

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

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

### 7.2 Pairs that belong together

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

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

### 7.3 Nesting that is always wrong

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

---

## §8 — Page recipes

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

### 8.1 Login screen

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

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

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

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

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

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

### 8.2 Settings page (mobile)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

### 8.4 Dashboard with a FAB

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

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

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

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

### 8.5 Destructive confirmation dialog

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

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

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

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

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

### 8.6 Tabbed content page

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

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

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

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

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

### 8.7 The full recipe library

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

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

---

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

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

### 9.1 API and styling

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

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

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

### 9.2 Content and hierarchy

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

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

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

### 9.3 Accessibility contract

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

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

---

## §10 — Before you ship

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