Skip to content

Autocomplete

Type to filter, then choose. A text field with a suggestion menu: single or multiple selection, optional free-text values, custom filtering, async loading, and virtualization for large option sets.

Live preview — type a letter to filter Open in Storybook
Paris Berlin Madrid Rome Lisbon Athens
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-autocomplete label="City" placeholder="Start typing…" style="inline-size: 280px;">
  <md-select-option value="par">Paris</md-select-option>
  <md-select-option value="ber">Berlin</md-select-option>
  <md-select-option value="mad">Madrid</md-select-option>
  <md-select-option value="rom">Rome</md-select-option>
  <md-select-option value="lis">Lisbon</md-select-option>
  <md-select-option value="ath">Athens</md-select-option>
</md-autocomplete>

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


<md-autocomplete></md-autocomplete>
  • A long or open-ended option set the user narrows by typing: cities, users, products, tags.
  • Values that may not exist yet, where the user can enter their own (free-solo).
  • Multi-value entry rendered as chips (multiple).
SituationUse instead
A short, closed listmd-select
Several values from a known closed listmd-multi-select
App-wide search with a results surfacemd-search
Plain text with no suggestionsmd-text-field
2–5 exclusive optionsmd-segmented-button / md-radio
Assigning a subset from a poolmd-transfer-list

Three sources, and slotted options win:

  1. Slotted md-select-option children — best for a static list you can write in markup.
  2. The options property — an array of { value, label, supportingText?, icon?, iconColor?, disabled? }, for data that comes from your state layer.
  3. The options attribute — the same array as a JSON string, for a page that has its data up front and no reason to reach for a script.
options as a JSON attribute — no script needed
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-autocomplete
  label="City"
  placeholder="Search cities…"
  style="inline-size: 300px;"
  options='[
  {"value":"lis","label":"Lisbon","supportingText":"Portugal"},
  {"value":"osl","label":"Oslo","supportingText":"Norway"},
  {"value":"kyo","label":"Kyoto","supportingText":"Japan"},
  {"value":"qro","label":"Querétaro","supportingText":"Mexico"}
  ]'
  ></md-autocomplete>
Slotted options with icons, supporting text and a disabled row Open in Storybook
Ada Lovelace Grace Hopper Alan Turing Katherine Johnson Linus Torvalds
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-autocomplete label="Assignee" placeholder="Search people…" style="inline-size: 300px;">
  <md-select-option value="ada" icon="person" supporting-text="Engineering">Ada Lovelace</md-select-option>
  <md-select-option value="grace" icon="person" supporting-text="Engineering">Grace Hopper</md-select-option>
  <md-select-option value="alan" icon="person" supporting-text="Research">Alan Turing</md-select-option>
  <md-select-option value="katherine" icon="person" supporting-text="Research">Katherine Johnson</md-select-option>
  <md-select-option value="linus" icon="person" disabled supporting-text="On leave">Linus Torvalds</md-select-option>
</md-autocomplete>

The single most common mix-up on this component.

HoldsReported by
valueThe committed selection — a string, or string[] when multiplemdChange
inputValueThe raw text in the boxmdInput

Reading value to get what the user typed returns the last selection, not the text. Reading mdInput as a selection fires on every keystroke.

filled is the default — the opposite of md-select, whose default is outlined.

Filled and outlined Open in Storybook
Alpha Bravo Charlie Alpha Bravo Charlie
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-autocomplete variant="filled" label="Filled" style="inline-size: 240px;">
  <md-select-option value="a">Alpha</md-select-option>
  <md-select-option value="b">Bravo</md-select-option>
  <md-select-option value="c">Charlie</md-select-option>
</md-autocomplete>
<md-autocomplete variant="outlined" label="Outlined" style="inline-size: 240px;">
  <md-select-option value="a">Alpha</md-select-option>
  <md-select-option value="b">Bravo</md-select-option>
  <md-select-option value="c">Charlie</md-select-option>
</md-autocomplete>

multiple turns value into a string[] and renders each pick as a chip. chip-position places the chip rail — below (default), top, left, right or inline. A multi-select menu already stays open after a pick, so several can be made in a row; disable-close-on-select is for single mode, and it also makes the popup persistent — no outside-click dismissal, only Escape, the trigger or a pick closes it.

Chips below the field, and inline in it Open in Storybook
Design Engineering Documentation Accessibility Performance Design Engineering Documentation
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-autocomplete multiple label="Tags" placeholder="Add tags…" style="inline-size: 320px;">
  <md-select-option value="design">Design</md-select-option>
  <md-select-option value="eng">Engineering</md-select-option>
  <md-select-option value="docs">Documentation</md-select-option>
  <md-select-option value="a11y">Accessibility</md-select-option>
  <md-select-option value="perf">Performance</md-select-option>
</md-autocomplete>
<md-autocomplete multiple chip-position="inline" label="Inline chips" placeholder="Add tags…" style="inline-size: 320px;">
  <md-select-option value="design">Design</md-select-option>
  <md-select-option value="eng">Engineering</md-select-option>
  <md-select-option value="docs">Documentation</md-select-option>
</md-autocomplete>

chip-position="left" / "right" are physical, not logical — re-check them in RTL.

free-solo lets value hold text that isn’t in options. The component accepts anything — validating it is yours.

Free-solo — commit text that isn't in the list Open in Storybook
bug feature chore
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-autocomplete free-solo label="Tag" placeholder="Pick one or invent one" style="inline-size: 300px;">
  <md-select-option value="bug">bug</md-select-option>
  <md-select-option value="feature">feature</md-select-option>
  <md-select-option value="chore">chore</md-select-option>
</md-autocomplete>
Type “wip” and commit it — free text is accepted, then flagged as invalid Open in Storybook
feat fix chore Commit a value — anything outside the list is flagged.
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-autocomplete id="tag" free-solo label="Tag">
  <md-select-option value="feat">feat</md-select-option>
  <md-select-option value="fix">fix</md-select-option>
  <md-select-option value="chore">chore</md-select-option>
</md-autocomplete>

<script type="module">
  const el = document.getElementById('tag');
  const known = new Set(['feat', 'fix', 'chore']);

  el.addEventListener('mdChange', (e) => {
    el.setCustomValidity(known.has(e.detail) ? '' : 'Unknown tag');
  });
</script>

clear-on-blur discards unmatched text when the field loses focus — strict single mode only: it is ignored when multiple or free-solo is set, and it never fires while the popup is open (arrowing into the listbox must not wipe the active filter).

Three distinct messages, and they are not interchangeable:

PropShown when
loading-textloading is true
no-options-textThere are no options at all
no-results-textThere are options, but nothing matched the query
Loading, empty option set, and no match — click each field to open its menu Open in Storybook
Paris Berlin
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-autocomplete loading loading-text="Fetching cities…" label="Loading" placeholder="Click to open" style="inline-size: 260px;"></md-autocomplete>
<md-autocomplete no-options-text="No cities configured" label="No options" placeholder="Click to open" style="inline-size: 260px;"></md-autocomplete>
<md-autocomplete no-results-text="No city matches that" label="No results" placeholder="Type zzz" style="inline-size: 260px;">
  <md-select-option value="par">Paris</md-select-option>
  <md-select-option value="ber">Berlin</md-select-option>
</md-autocomplete>

Slot your own into loader when the default spinner isn’t the right affordance — a circular md-progress-indicator, a branded mark, a skeleton. Everything else about the loading state is unchanged.

The built-in md-loading-indicator, and an md-progress-indicator slotted over 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>

<md-autocomplete loading loading-text="Fetching…" label="Default loader" style="inline-size: 260px;"></md-autocomplete>

<md-autocomplete loading loading-text="Fetching…" label="Slotted loader" style="inline-size: 260px;">
  <md-progress-indicator slot="loader" variant="circular" indeterminate size="24" label="Loading"></md-progress-indicator>
</md-autocomplete>
Disabled, soft-disabled, error and supporting text Open in Storybook
Alpha Bravo Alpha
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-autocomplete label="Enabled" style="inline-size: 240px;">
  <md-select-option value="a">Alpha</md-select-option>
  <md-select-option value="b">Bravo</md-select-option>
</md-autocomplete>
<md-autocomplete label="Disabled" disabled style="inline-size: 240px;"></md-autocomplete>
<md-autocomplete label="Soft-disabled" soft-disabled style="inline-size: 240px;"></md-autocomplete>
<md-autocomplete label="Error" error error-text="Pick a city" required style="inline-size: 240px;"></md-autocomplete>
<md-autocomplete label="Supporting text" supporting-text="Start typing to filter" reserve-supporting-space style="inline-size: 240px;">
  <md-select-option value="a">Alpha</md-select-option>
</md-autocomplete>

reserve-supporting-space holds the line height so the layout doesn’t jump when an error appears. Show supporting text or error text — never both.

filter-mode picks a built-in matching strategy (substring by default). filterer replaces matching entirely and is a function property:

Type “ge” — the prefix filterer keeps Germany and Georgia, and drops Algeria Open in Storybook
substring would match “Algeria” on “ge” — this prefix filterer does not.
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-autocomplete id="country" label="Country"></md-autocomplete>

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

  // Function property — there is no filterer attribute.
  el.filterer = (options, { inputValue }) =>
    options.filter((o) => o.label.toLowerCase().startsWith(inputValue.toLowerCase()));
</script>

limit-results caps how many suggestions render on the client-side path — a rendering guard, not a search limit, since matching still scans every option. The virtualized path ignores it: the WASM listbox windows its own rows.

virtualize="always" renders rows on demand; pair it with row-height so the scroller can size itself. virtualize="auto" (the default) decides for you.

For a dataset that big, don’t hand it an array — options would mean materialising every row on the JS heap. loadOptions({ count, getRow }) takes a row factory and packs the rows into the WASM store one at a time, so the data never exists in JS. Filtering and scrolling then run against the packed store.

Pick a dataset size — 100k, 1M or 10M rows Open in Storybook
100k 1M 10M

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

<md-autocomplete
  id="options"
  label="Option"
  virtualize="always"
  row-height="48"
  ></md-autocomplete>

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

  // A row FACTORY, not an array: loadOptions packs the dataset into the WASM
  // store one row at a time, so the rows never exist on the JS heap.
  // Keep the factory cheap — it runs once per row. Formatting each label with
  // toLocaleString() added 3.4s to a 10M pack in this very demo.
  await el.loadOptions({
    count: 10_000_000,
    getRow: (i) => ({ value: 'v' + i, label: 'Option ' + i }),
  });
</script>
EventCancelableDetailFires
mdInputnostringEvery change to the typed text
mdChangenostring | string[]The committed selection changes
mdOpen / mdClosenovoidThe suggestion menu opens / closes
mdClearnovoidThe clear affordance is used
mdValidityChangeno{ valid, validationMessage, flags }Validity changes — not composed
Type “lo” — mdInput debounces, loading shows, then the options arrive Open in Storybook
mdInput carries the TYPED TEXT — each keystroke debounces a search.
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-autocomplete id="city" label="City" name="city" required></md-autocomplete>

<script type="module">
  const el = document.getElementById('city');
  el.options = [];

  let t;
  el.addEventListener('mdInput', (e) => {       // e.detail is the TYPED TEXT
    clearTimeout(t);
    t = setTimeout(async () => {
      el.loading = true;
      el.options = await searchCities(e.detail);
      el.loading = false;
    }, 250);
  });

  el.addEventListener('mdChange', (e) => save(e.detail));   // committed selection
</script>

The component is form-associated via ElementInternals, so name and required participate in FormData and in native constraint validation. value-missing-label is the message shown when a required field is empty. getValidity(), checkValidity(), reportValidity() and setCustomValidity() are available as methods.

Submit empty and required blocks it; pick a city and FormData carries it — Reset restores the initial value Open in Storybook
London Paris Berlin
Save Reset checkValidity()
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<form id="signup">
  <md-autocomplete
    id="city"
    name="city"
    label="City"
    required
    value-missing-label="Pick a city"
    >
    <md-select-option value="lon">London</md-select-option>
    <md-select-option value="par">Paris</md-select-option>
  </md-autocomplete>

  <md-button variant="filled" type="submit">Save</md-button>
  <md-button variant="text" type="reset">Reset</md-button>
</form>

<script type="module">
  const form = document.getElementById('signup');
  const city = document.getElementById('city');

  form.addEventListener('submit', (e) => {
    e.preventDefault();
    // name/value land in FormData across the shadow boundary — no hidden input.
    console.log(Object.fromEntries(new FormData(form)));   // { city: 'lon' }
  });

  // Same validity surface as a native control.
  const { valid, validationMessage } = await city.getValidity();
</script>

Properties

Property Attribute Type Default Reflects
valueMissingLabel value-missing-label string 'Please make a selection' —
reserveSupportingSpace reserve-supporting-space boolean false —
variant variant 'filled' | 'outlined' 'filled' Yes
label label string '' —
placeholder placeholder string '' —
supportingText supporting-text string '' —
error error boolean false Yes
errorText error-text string '' —
disabled disabled boolean false Yes
softDisabled soft-disabled boolean false Yes
required required boolean false Yes
density density 0 | -1 | -2 | -3 | -4 0 Yes
options options MdAutocompleteOption[] | string [] —
multiple multiple boolean false Yes
chipPosition chip-position | 'below' | 'top' | 'left' | 'right' | 'inline' 'below' Yes
value value string | string[] '' —
inputValue input-value string '' —
freeSolo free-solo boolean false Yes
maxSelected max-selected number 0 —
loading loading boolean false Yes
noOptionsText no-options-text string 'No options' —
noResultsText no-results-text string 'No results' —
loadingText loading-text string 'Loading…' —
statusTemplate status-template string '{count} suggestions available' —
disableCloseOnSelect disable-close-on-select boolean false Yes
clearOnBlur clear-on-blur boolean false Yes
clearable clearable boolean true Yes
clearIcon clear-icon string 'close' —
dropdownIcon dropdown-icon string 'arrow_drop_down' —
limitResults limit-results number 0 —
open open boolean false Yes
filterer JS only ( options: SelectOptionData[], state: { inputValue: string — —
virtualize virtualize 'auto' | 'always' | 'never' 'auto' —
filterMode filter-mode FilterMode 'substring' —
rowHeight row-height number — —
placement placement 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end' 'bottom-start' —
matchTriggerWidth match-trigger-width boolean true —
maxHeight max-height number — —
name name string '' Yes

Methods

Method Parameters
focusInput() none
showMenu() none
closeMenu() none
loadOptions() source: SelectOptionInit[] | OptionRowSource
getLabels() values: string[]
getValidity() none
checkValidity() none
reportValidity() none
setCustomValidity() message: string

Slots

Slot Description
loader Replaces the default trigger busy spinner
dropdown-icon Replaces the caret glyph

CSS Custom Properties

Override on the host element for per-instance theming:

Property Description
--md-autocomplete-width Trigger width (default 100% of container)
--md-autocomplete-min-width Minimum trigger width
--md-autocomplete-chip-gap Gap between selected chips
--md-autocomplete-chip-radius Chip corner radius
--md-autocomplete-caret-color Trailing caret color
--md-autocomplete-caret-size Caret glyph size (default 24px)
--md-autocomplete-clear-color Clear button color
--md-autocomplete-option-icon-size Leading-icon size in items
--md-autocomplete-loading-color Color of the loading row
--md-autocomplete-spinner-size Trigger busy-spinner size (default 22px)
--md-autocomplete-option-icon-color —

CSS Shadow Parts

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

Part Description
chip —
chips Selected chips container
clear Trailing clear button
field Inner md-text-field
loading-spinner Trigger busy spinner (replaces clear/caret)
caret Trailing dropdown caret
option-icon —
menu Popup md-menu
loading Loading row inside the menu
loading-progress —
chip-remove —
chip-label —
  • label names the field. The suggestion count is announced through a live region driven by status-template — keep the {count} placeholder when you translate it.
  • Arrow keys move through suggestions, Enter commits, Escape closes.
  • Chips name their own remove control — Remove {label}. Note that name and the clear button’s "Clear value" are hard-coded English; neither has a prop yet.
  • reserve-supporting-space avoids layout jump when errors appear.

RTL — field, chips, caret and menu mirror under dir="rtl". chip-position="left" / "right" is physical, so re-check it per direction. See RTL.

Density — density="-1…-4" compacts the field, the chips and the suggestion rows. Chips bottom out at a 24px floor (a tap-target guard in md-chip), so they stop shrinking around -2 while the field and rows keep going. See Density.

Every rung, 0 through -4 — the field and the suggestion rows taper; chips stop at their 24px floor 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-autocomplete multiple label="Tags"></md-autocomplete>
<md-autocomplete multiple density="-1" label="Tags"></md-autocomplete>
<md-autocomplete multiple density="-2" label="Tags"></md-autocomplete>
<md-autocomplete multiple density="-3" label="Tags"></md-autocomplete>
<md-autocomplete multiple density="-4" label="Tags"></md-autocomplete>

<script type="module">
  const tags = [
    { value: 'design', label: 'Design' },
    { value: 'eng', label: 'Engineering' },
    { value: 'prd', label: 'Product' },
    { value: 'ops', label: 'Operations' },
  ];

  // options also takes a JSON attribute; value has no attribute form, so it is
  // assigned here. Every framework below binds both directly instead.
  for (const el of document.querySelectorAll('md-autocomplete')) {
    el.options = tags;
    el.value = ['design', 'eng'];
  }
</script>

i18n — translate label, placeholder, supporting-text, error-text, no-options-text, no-results-text, loading-text, value-missing-label, and status-template (keeping {count}). Every one of them defaults to English.

Custom propertyPurposeDefault
--md-autocomplete-width / --md-autocomplete-min-widthField inline-sizeauto
--md-autocomplete-chip-gap / --md-autocomplete-chip-radiusChip rail metricsPer density
--md-autocomplete-caret-color / --md-autocomplete-caret-sizeDropdown careton-surface-variant
--md-autocomplete-clear-colorClear affordanceon-surface-variant
--md-autocomplete-option-icon-size / --md-autocomplete-option-icon-colorSuggestion row icons24px
--md-autocomplete-loading-color / --md-autocomplete-spinner-sizeLoading stateprimary
Themed instance Open in Storybook
Alpha Bravo Charlie
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-autocomplete
  label="Themed"
  variant="outlined"
  style="--md-autocomplete-width: 300px; --md-autocomplete-caret-color: var(--md-sys-color-primary); --md-autocomplete-chip-radius: 4px;">
  <md-select-option value="a">Alpha</md-select-option>
  <md-select-option value="b">Bravo</md-select-option>
  <md-select-option value="c">Charlie</md-select-option>
</md-autocomplete>

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

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

<md-autocomplete label="Untouched defaults" style="min-inline-size: 280px;">
  <md-select-option value="lon">London</md-select-option>
  <md-select-option value="par">Paris</md-select-option>
  <md-select-option value="ber">Berlin</md-select-option>
</md-autocomplete>
PartElement
fieldThe text-field trigger
chips / chipSelected-value chips, in multiple mode
chip-remove / chip-labelForwarded out of each chip (exportparts)
clear / caretTrailing controls
menuThe suggestion popup
option / option-selectedSuggestion rows
option-iconPer-option icon
loading-spinnerTrailing spinner while busy
loading / loading-progressThe in-menu progress affordance
caret, and chip once you pick a value
London Paris
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<style>
  .parts-ac::part(caret) { color: var(--md-sys-color-primary); }
  .parts-ac::part(chip) { border-radius: 4px; }
</style>
<md-autocomplete class="parts-ac" multiple label="Styled parts" style="min-inline-size: 280px;">
  <md-select-option value="lon">London</md-select-option>
  <md-select-option value="par">Paris</md-select-option>
</md-autocomplete>

md-select · md-multi-select · md-search · md-text-field · md-chip · md-menu · md-transfer-list

For AI Agents — md-autocomplete

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

# md-autocomplete

<!-- llm:meta
tag: md-autocomplete
category: text-input
status: custom
m3-guidelines: none — M3 has no autocomplete page
m3-derived-from: https://m3.material.io/components/text-fields/guidelines
reference-parity: https://mui.com/material-ui/react-autocomplete/
form-associated: true
depends-on: md-chip, md-icon-button, md-text-field, md-loading-indicator, md-menu-item, md-menu, md-progress-indicator
used-by: none
accepts-children: md-select-option
-->

**Type to filter, then choose.** A text field with a suggestion listbox: single
or multiple selection, optional free-text values, custom filtering, async
loading, and virtualization for large option sets.

> ⚠️ **Not a Material Design 3 component.** M3 has no autocomplete page; the
> API deliberately mirrors MUI Autocomplete. The Do/Don't table below is house
> rules, informed by M3's menu and text-field guidance.

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

---

## When to use

- A long or open-ended option set the user narrows **by typing**: cities,
  users, products, tags.
- Values that may not exist yet, where the user can enter their own
  (`free-solo`).
- Multi-value entry rendered as chips (`multiple`).

## When NOT to use

| Situation | Use instead |
|---|---|
| A short, closed list | `md-select` |
| Several values from a **known** closed list | `md-multi-select` |
| App-wide search with a results surface | `md-search` |
| Plain text with no suggestions | `md-text-field` |
| 2–5 exclusive options | `md-segmented-button-set` / `md-radio` |
| Assigning a subset from a pool | `md-transfer-list` |

## Decision cues

| Need | Setting |
|---|---|
| Options in markup | `md-select-option` children |
| Options from data | `options` property (array in JS, or a JSON-array attribute) |
| Multiple values as chips | `multiple` (+ `chip-position`) |
| Chips inside the field | `chip-position="inline"` |
| Allow values not in the list | `free-solo` |
| Keep a **single**-select menu open across picks | `disable-close-on-select` (`multiple` already stays open) |
| Custom matching logic | `filterer` (function property, client-side path only) |
| Built-in matching strategy for huge lists | `filter-mode` |
| Cap how many suggestions render | `limit-results` |
| Cap how many can be selected | `max-selected` |
| Discard unmatched text on blur | `clear-on-blur` |
| Huge option sets | `virtualize="always"` + `row-height` |

## API contract

```html
<md-autocomplete
  variant="filled|outlined"                 <!-- default: filled -->
  label="City"
  placeholder="Start typing…"
  input-value=""                            <!-- live text in the box -->
  name="city"
  required                                  <!-- default: false -->
  multiple                                  <!-- default: false -->
  chip-position="below|top|left|right|inline"   <!-- default: below -->
  free-solo                                 <!-- default: false -->
  max-selected="0"                          <!-- default: 0 = unlimited -->
  clearable                                 <!-- default: TRUE -->
  clear-icon="close"                        <!-- default: close -->
  dropdown-icon="arrow_drop_down"           <!-- default: arrow_drop_down -->
  clear-on-blur                             <!-- default: false -->
  disable-close-on-select                   <!-- default: false -->
  filter-mode="substring"                   <!-- default: substring -->
  limit-results="0"                         <!-- default: 0 = unlimited -->
  virtualize="auto|always|never"            <!-- default: auto -->
  row-height="48"                           <!-- default: measured from row 1 -->
  placement="bottom-start|bottom-end|top-start|top-end"   <!-- default: bottom-start -->
  match-trigger-width                       <!-- default: true -->
  max-height="320"                          <!-- pixels (number) -->
  loading                                   <!-- default: false -->
  loading-text="Loading…"                   <!-- default: Loading… -->
  no-options-text="No options"              <!-- default: No options -->
  no-results-text="No results"              <!-- default: No results -->
  status-template="{count} suggestions available"
  supporting-text="" error error-text=""
  reserve-supporting-space                  <!-- default: false -->
  value-missing-label="Please make a selection"
  disabled                                  <!-- default: false -->
  soft-disabled                             <!-- default: false -->
  open                                      <!-- default: false -->
  density="-1|-2|-3|-4"                     <!-- default: 0 = uncompacted -->
>
  <md-select-option value="par">Paris</md-select-option>
  <md-select-option value="ber">Berlin</md-select-option>
</md-autocomplete>
```

```js
// value (array form) and filterer are JS properties.
const el = document.querySelector('md-autocomplete');
el.options = cities.map((c) => ({ value: c.id, label: c.name }));
el.value = 'par';                     // string, or string[] when `multiple`
el.filterer = (options, { inputValue }) =>
  options.filter((o) => o.label.toLowerCase().startsWith(inputValue.toLowerCase()));
```

**Events** — `mdInput` (`CustomEvent<string>`, the typed text), `mdChange`
(`string | string[]`, the committed selection), `mdOpen` (`void`), `mdClose`
(`void`), `mdClear` (`void`) — all composed; `mdValidityChange`
(`{ valid, validationMessage, flags }`) bubbles but is **not composed**.

**Methods** — `focusInput()`, `showMenu()`, `closeMenu()`,
`loadOptions(source)`, `getLabels(values)`, `getValidity()`,
`checkValidity()`, `reportValidity()`, `setCustomValidity(message)`. All are
async. There is **no** `setQuery()` here, and no programmatic way to filter:
the query is only what the user typed since the popup opened. Assigning
`el.inputValue = 'par'` changes the visible text but leaves the query empty,
so the list stays unfiltered.

**Slots** — `loader` (replaces the trigger busy spinner) and `dropdown-icon`
(replaces the caret glyph). There is **no default slot**: `md-select-option`
children are read out of the light DOM as data carriers and are never
projected (they are `display: none` anyway).

**Parts** — `field`, `chips`, `chip`, `clear`, `caret`, `menu`, `option`,
`option-selected`, `option-icon`, `loading`, `loading-spinner`,
`loading-progress`. Forwarded out of `md-chip`: `chip-remove`, `chip-label`.
`option-selected` is applied alongside `option` on selected rows.

### Behavioral contract worth knowing

- **`value` and `inputValue` are different things.** `value` is the committed
  selection (a `string`, or `string[]` when `multiple`); `inputValue` is the
  raw text in the box. `mdInput` reports typing, `mdChange` reports selection.
  Typing never uncommits `value`.
- **Two option sources, with a defined winner.** Slotted `md-select-option`
  children take precedence; `options` is only read when there are none.
- `options` accepts the array as a **JS property** or a **JSON array string**
  as an attribute (`options='[{"value":"a","label":"Apple"}]'`). Malformed JSON
  warns on the console and degrades to an empty list. `description` is accepted
  as a legacy alias for `supportingText` on an entry.
- `filterer` and the array form of `value` are **JS properties** — a function
  and an array have no attribute form.
- Unlike `md-select` / `md-multi-select`, this component **ignores the
  `selected` hint** on `md-select-option` children (and the `selected` flag on
  an `options` entry). Set `value` to preselect, and `input-value` for the text
  that should show. Removing a child `md-select-option` at runtime also goes
  unnoticed here — there is no default slot to observe — so drive a shrinking
  option set from `options`.
- `clearable` defaults to **`true`** here (unlike `md-select` /
  `md-multi-select`, where it is `false`).
- Strict single mode (neither `multiple` nor `free-solo`): closing the popup
  snaps `inputValue` back to the committed option's label, or to `''` when
  nothing is committed — typed-but-uncommitted text never lingers.
- `clear-on-blur` only applies in that strict single mode, and only when the
  popup is already closed.
- Reopening the popup shows the **full** list: the filter only counts text
  typed since the popup opened, so a committed value does not filter the list
  down to itself.
- `free-solo` lets `value` hold text that is not in `options`; `Enter` commits
  the trimmed input. The component validates nothing — do it yourself.
- `max-selected="0"` means **unlimited**. When the cap is reached a pick is
  rejected, and the row (which self-toggles on click before the parent
  reconciles) is reverted via the event target.
- `limit-results` caps how many suggestions *render* on the client-side path
  only; matching still scans everything, and the virtualized path ignores it.
- `filterer` is ignored while virtualized — the WASM engine filters by
  `filter-mode` instead.
- `virtualize="auto"` switches to the virtualized path above 200 options. A
  virtualized menu needs a bounded viewport, so `max-height` defaults to `320`
  when unset. While a dataset is packing the component presents the same busy
  UI as `loading`.
- Keyboard: `ArrowDown` / `ArrowUp` open the popup and move **real focus** onto
  the first/last option; typing while an option has focus returns focus to the
  input and extends the query; `Enter` commits a free-solo value; `Escape`
  closes; `Backspace` on an empty `multiple` input removes the last chip.
- `multiple` stays open after each pick and clears the typed text; single mode
  closes unless `disable-close-on-select` is set.
- Flipping `multiple` at runtime reshapes `value` (string ⇄ array) and
  re-publishes the form value.
- `required` is published to the owning form through `ElementInternals`, so an
  empty required control really blocks submission. `error-text`, when set, is
  used as the validation message too. `setCustomValidity(msg)` wins over both;
  clear it with `''`.
- `mdValidityChange` never fires on mount, repeats nothing, and is **not
  composed** — listen on the `md-autocomplete` element itself. `mdOpen` /
  `mdClose` **are** composed, so they also fire on an embedding shadow host.
- **The combobox spans two shadow roots**, and is wired accordingly. The role,
  the expanded state and the typing relationship go to `md-text-field` as props
  — `input-role="combobox"`, `input-expanded`, `input-aria-autocomplete="list"`
  — so they land on the real `<input>`, the element assistive tech reads. The
  listbox is referenced with ARIA **element reflection**
  (`input.ariaControlsElements`), because an IDREF would have to resolve inside
  `md-text-field`'s tree, where the listbox is not. The reference exists only
  while the popup is open and is cleared on close. Where element reflection is
  unavailable you still get the role, a truthful expanded state, and the
  focus-moves-into-the-listbox pattern, which needs no reference at all.
- Automated checkers that read attributes rather than the accessibility tree
  may report `aria-required-attr` while the popup is open, because the
  reflection setter leaves an empty `aria-controls=""` behind. Do **not** "fix"
  it by writing the listbox id into that attribute: doing so clears the element
  reference and trades a real relation for a green checker.

---

## Do / Don't

House rules, informed by
[M3 · Menus · Guidelines](https://m3.material.io/components/menus/guidelines)
and
[M3 · Text fields · Guidelines](https://m3.material.io/components/text-fields/guidelines).

| ✅ Do | ❌ Don't |
|---|---|
| Always set a `label` | Don't use `placeholder` as the label |
| Debounce, or use `loadOptions`, for remote search | Don't fetch on every keystroke unthrottled |
| Show `loading` while suggestions are in flight | Don't leave an empty menu with no explanation |
| Distinguish "no options" from "no results" | Don't reuse one message for both states |
| Use `free-solo` only when arbitrary values are genuinely valid | Don't allow free text into a closed vocabulary |
| Validate free-solo input before saving | Don't trust an unmatched value |
| Cap rendering with `limit-results` on big sets | Don't render thousands of rows unvirtualized |
| Keep option labels short and scannable | Don't put sentences in suggestion rows |
| Localize every text prop | Don't ship the English defaults |
| Swap supporting text for error text | Don't show both |

---

## Patterns

```html
<!-- Options in markup -->
<md-autocomplete label="City" name="city" required>
  <md-select-option value="par">Paris</md-select-option>
  <md-select-option value="ber" supporting-text="Germany">Berlin</md-select-option>
  <md-select-option value="mad">Madrid</md-select-option>
</md-autocomplete>
```

```html
<!-- Remote search, debounced -->
<md-autocomplete id="city" label="City" name="city" required></md-autocomplete>

<script type="module">
  const el = document.getElementById('city');
  el.options = [];

  let t;
  el.addEventListener('mdInput', (e) => {          // typed text
    clearTimeout(t);
    t = setTimeout(async () => {
      el.loading = true;
      el.options = await searchCities(e.detail);
      el.loading = false;
    }, 250);
  });

  el.addEventListener('mdChange', (e) => console.log(e.detail)); // selection
</script>
```

```html
<!-- Multiple values as chips, capped. Multi stays open on every pick; it
     dismisses on an outside click, on the trigger, or on Escape. -->
<md-autocomplete label="Tags" multiple max-selected="5" chip-position="below">
  <md-select-option value="new">New</md-select-option>
  <md-select-option value="urgent">Urgent</md-select-option>
</md-autocomplete>
```

```html
<!-- Free text allowed — validate it yourself -->
<md-autocomplete id="tag" free-solo clear-on-blur label="Tag"></md-autocomplete>

<script type="module">
  const el = document.getElementById('tag');
  const known = new Set(['alpha', 'beta']);
  el.options = [...known].map((v) => ({ value: v, label: v }));

  el.addEventListener('mdChange', (e) => {
    el.setCustomValidity(known.has(e.detail) ? '' : 'Unknown tag');
  });
</script>
```

```html
<!-- Custom matching (prefix instead of substring), client-side path.
     `filterer` runs only while NOT virtualized, so keep the set small
     enough that `virtualize="auto"` stays on the client path (<= 200),
     or pin it with virtualize="never". -->
<md-autocomplete id="users" label="User"
                 virtualize="never" limit-results="50">
</md-autocomplete>

<script type="module">
  const el = document.getElementById('users');
  el.options = await fetchUsers();                 // a few hundred at most
  el.filterer = (opts, { inputValue }) =>
    opts.filter((o) => o.label.toLowerCase().startsWith(inputValue.toLowerCase()));
</script>
```

```html
<!-- Large virtualized set. The WASM engine filters here: `filter-mode`
     picks the matching strategy, and `filterer` / `limit-results` are
     both ignored on this path. -->
<md-autocomplete id="big-users" label="User"
                 virtualize="always" filter-mode="prefix" row-height="48">
</md-autocomplete>

<script type="module">
  const el = document.getElementById('big-users');
  await el.loadOptions(await fetchUsers());        // tens of thousands
</script>
```

## Anti-patterns

| ❌ Wrong | ✅ Right | Why |
|---|---|---|
| Treating `mdInput` as the selection | `mdInput` is typing; `mdChange` is selection | Two distinct signals. |
| Reading `value` to get the typed text | Use `inputValue` | `value` is the committed selection. |
| Setting `inputValue` to filter the list | Filter the data you pass to `options` (or use `filterer` / `filter-mode`) | Only text typed by the user counts as the query; assigning `inputValue` does not filter. |
| `filterer` as an HTML attribute | Assign the function in JS | A function has no attribute form. `options` does have one: a JSON array string. |
| `max-selected="0"` to block selection | `0` = unlimited; use `disabled` | Same trap as `md-multi-select`. |
| `limit-results` as a search limit | It caps **rendering**, on the client-side path only | Matching still scans everything. |
| Expecting `filterer` to run on a virtualized list | Use `filter-mode` | The WASM engine does the filtering there. |
| Unvalidated `free-solo` values | Validate in `mdChange` | The component accepts anything. |
| Fetching on every keystroke | Debounce, or use `loadOptions` | Hammering the backend. |
| Assuming `clearable` is off by default | It defaults to `true` — set `clearable="false"` to hide it | Unlike the selects. |
| Setting `role="combobox"` on the host from outside | It is already a combobox — on the inner input | A role on the host would wrap the real control in a second announcement. |
| Expecting `value` to be an array without `multiple` | It is a string in single mode | The type changes with `multiple`. |
| Supplying `md-select-option` children **and** `options` | Pick one source | Children win outright; the array is silently ignored. |
| Shipping English `no-results-text` etc. | Translate every text prop | They all default to English. |
| Using it for site-wide search | `md-search` | Different surface and semantics. |

## Accessibility, RTL, density, i18n

**Accessibility**
- `label` names the field. The visible suggestion count is announced through a
  polite live region driven by `status-template`; keep `{count}` when
  translating it.
- Arrow keys move focus into the suggestion list, `Enter` commits, `Escape`
  closes. Typing while an option has focus returns to the input.
- The real textbox reports `role="combobox"`, `aria-expanded` and
  `aria-autocomplete="list"`, and controls the listbox through an element
  reference — see the contract note for how that crosses the shadow boundary.
- Chips sit in a labelled `role="group"`; each ✕ is named `Remove {label}`.
- `reserve-supporting-space` avoids a layout jump when errors appear.

**RTL** — field, chips, caret and menu mirror under `dir="rtl"`.
`chip-position="left"` / `"right"` is **physical** — re-check per direction;
`placement` is logical.

**Density** — `density="-1"` … `density="-4"` compacts the field, the chips and
the rows. There is no `density="0"` rung: 0 is the uncompacted default and
setting it does nothing. To opt out of an inherited `data-density` rung, set
`style="--md-sys-density-scale: 0"` on the element — that only resets the
calc-driven scale, not the spacing tokens the ancestor rung declares.

**i18n** — translate `label`, `placeholder`, `supporting-text`, `error-text`,
`no-options-text`, `no-results-text`, `loading-text`, `value-missing-label`,
and `status-template` (keeping `{count}`). Two accessible names are **not**
localizable yet: the clear button's `"Clear value"` and each chip's
`"Remove {label}"` are hard-coded English.

## Related components

`md-select` · `md-multi-select` · `md-select-option` · `md-search` ·
`md-text-field` · `md-chip` · `md-menu` · `md-transfer-list`

## Theming

| Custom property | Purpose | Default |
|---|---|---|
| `--md-autocomplete-width` | Host inline size | `100%` |
| `--md-autocomplete-min-width` | Host minimum inline size | `220px` |
| `--md-autocomplete-chip-gap` | Gap between selection chips | `--md-sys-spacing-gap-xs` (4px) |
| `--md-autocomplete-chip-radius` | Chip corner radius | `8px` |
| `--md-autocomplete-caret-color` | Trailing caret colour | `--md-sys-color-on-surface-variant` |
| `--md-autocomplete-caret-size` | Caret glyph size | `24px` |
| `--md-autocomplete-clear-color` | Clear button colour | `--md-sys-color-on-surface-variant` |
| `--md-autocomplete-option-icon-color` | Suggestion leading-icon colour | `--md-sys-color-on-surface-variant` |
| `--md-autocomplete-option-icon-size` | Suggestion leading-icon size | `20px` |
| `--md-autocomplete-loading-color` | In-menu loading row colour | `--md-sys-color-on-surface-variant` |
| `--md-autocomplete-spinner-size` | Trigger busy-spinner size | `22px` |

**CSS parts** — `field`, `chips`, `chip`, `chip-remove`, `chip-label`, `clear`,
`caret`, `menu`, `option`, `option-selected`, `option-icon`, `loading`,
`loading-spinner`, `loading-progress`.

The composed field, chips and menu also expose their own `--md-text-field-*`,
`--md-chip-*`, `--md-menu-*` and `--md-menu-item-*` properties, which flow
through when set on the host.

```css
md-autocomplete {
  --md-autocomplete-min-width: 320px;
  --md-autocomplete-caret-color: var(--md-sys-color-primary);
}

md-autocomplete::part(option-selected) {
  font-weight: 600;
}
```

<!-- Auto Generated Below -->


## Overview

`md-autocomplete` — Material Design 3 combobox: a text field that filters
a dropdown listbox **as you type in the input** (no separate search row),
with single or multiple (chips) selection.

Built from the same primitives as `md-select` / `md-multi-select`:
  - the shared option model (`options` array of `{ value, label,
    supportingText?, icon?, iconColor?, disabled? }` or slotted
    `<md-select-option>` children),
  - `md-text-field` for the trigger (typing filters live),
  - `md-menu` as a WAI-ARIA listbox popup (options carry `aria-selected`),
  - `VirtualSelectController` for WASM-virtualized huge datasets
    (`virtualize`, `loadOptions()`, `filter-mode`) — the same engine as
    the selects, so tens of thousands of rows filter smoothly.

Keyboard: typing filters; ArrowDown moves focus into the listbox (the
focus-moves-into-popup combobox variant — option focus is announced
directly); Enter selects; Escape closes back to the input; Backspace on an
empty multi input removes the last chip.

ARIA: the real textbox — the `<input>` inside md-text-field's shadow root — is
promoted to `role="combobox"` with `aria-expanded` / `aria-autocomplete`
(handed over via `input-role`, since only that component renders the input),
and points at this component's listbox through ARIA element reflection. An
IDREF could not: it would have to resolve inside md-text-field's tree, where
the listbox is not.

Extras kept from the surface this API mirrors: `free-solo` (commit arbitrary text),
`max-selected`, `clearable`, `disable-close-on-select`, `clear-on-blur`,
a custom `filterer` and `limit-results` (client-side path only).

## Properties

| Property                 | Attribute                  | Description                                                                                                                                                                                                                                | Type                                                                                                                     | Default                           |
| ------------------------ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | --------------------------------- |
| `chipPosition`           | `chip-position`            | Where the selection chips live (`multiple` only): around the field (`'below'` default, `'top'`, `'left'`, `'right'`) or `'inline'` inside it, before the input, wrapping onto new rows as the selection grows (the field grows with them). | `"below" \| "inline" \| "left" \| "right" \| "top"`                                                                      | `'below'`                         |
| `clearIcon`              | `clear-icon`               | Material Symbols glyph for the clear button.                                                                                                                                                                                               | `string`                                                                                                                 | `'close'`                         |
| `clearOnBlur`            | `clear-on-blur`            | Clear a non-matching input on blur (strict single mode only).                                                                                                                                                                              | `boolean`                                                                                                                | `false`                           |
| `clearable`              | `clearable`                | Show a clear (×) button on the trailing edge.                                                                                                                                                                                              | `boolean`                                                                                                                | `true`                            |
| `density`                | `density`                  | Density forwarded to the text-field.                                                                                                                                                                                                       | `-1 \| -2 \| -3 \| -4 \| 0`                                                                                              | `0`                               |
| `disableCloseOnSelect`   | `disable-close-on-select`  | Keep the menu open after a selection in **single** mode. `multiple` already stays open — picking several in a row is the point, and it dismisses on an outside click, on the trigger, or on Escape. This prop gives a single-select the same stickiness when a picker is being used to scan rather than to commit.                                                                                                                                                                       | `boolean`                                                                                                                | `false`                           |
| `disabled`               | `disabled`                 | Disabled — non-interactive.                                                                                                                                                                                                                | `boolean`                                                                                                                | `false`                           |
| `dropdownIcon`           | `dropdown-icon`            | Caret glyph; rotates 180° when open (matches md-select). Slot `dropdown-icon` overrides it.                                                                                                                                                | `string`                                                                                                                 | `'arrow_drop_down'`               |
| `error`                  | `error`                    | Error state — forwarded to the text-field.                                                                                                                                                                                                 | `boolean`                                                                                                                | `false`                           |
| `errorText`              | `error-text`               | Error text rendered in place of supporting text when `error` is true.                                                                                                                                                                      | `string`                                                                                                                 | `''`                              |
| `filterMode`             | `filter-mode`              | Filter strategy for the virtualized (WASM) path.                                                                                                                                                                                           | `"fuzzy" \| "prefix" \| "substring"`                                                                                     | `'substring'`                     |
| `filterer`               | --                         | Custom client-side filter (ignored while virtualized — the WASM engine filters by `filter-mode` instead).                                                                                                                                  | `((options: SelectOptionData[], state: { inputValue: string; selected: string[]; }) => SelectOptionData[]) \| undefined` | `undefined`                       |
| `freeSolo`               | `free-solo`                | Allow committing arbitrary text (not just options) with Enter.                                                                                                                                                                             | `boolean`                                                                                                                | `false`                           |
| `inputValue`             | `input-value`              | Live text of the input (separate from `value` so typing never commits).                                                                                                                                                                    | `string`                                                                                                                 | `''`                              |
| `label`                  | `label`                    | Floating label / accessible name.                                                                                                                                                                                                          | `string`                                                                                                                 | `''`                              |
| `limitResults`           | `limit-results`            | Max items shown in the dropdown (`0` = unlimited; client-side path only).                                                                                                                                                                  | `number`                                                                                                                 | `0`                               |
| `loading`                | `loading`                  | Busy state: progress bar in the menu while an async dataset loads.                                                                                                                                                                         | `boolean`                                                                                                                | `false`                           |
| `loadingText`            | `loading-text`             | Text shown while `loading` is true.                                                                                                                                                                                                        | `string`                                                                                                                 | `'Loading…'`                      |
| `matchTriggerWidth`      | `match-trigger-width`      | Match the dropdown width to the trigger.                                                                                                                                                                                                   | `boolean`                                                                                                                | `true`                            |
| `maxHeight`              | `max-height`               | Max height of the dropdown menu (px).                                                                                                                                                                                                      | `number \| undefined`                                                                                                    | `undefined`                       |
| `maxSelected`            | `max-selected`             | When `multiple`, the max number of selected items (`0` = no limit).                                                                                                                                                                        | `number`                                                                                                                 | `0`                               |
| `multiple`               | `multiple`                 | Multi-select mode — the selection renders as removable chips.                                                                                                                                                                              | `boolean`                                                                                                                | `false`                           |
| `name`                   | `name`                     | Form name. Multi-mode submits as repeated `name=value` pairs.                                                                                                                                                                              | `string`                                                                                                                 | `''`                              |
| `noOptionsText`          | `no-options-text`          | Empty-state text when the dataset has no options at all.                                                                                                                                                                                   | `string`                                                                                                                 | `'No options'`                    |
| `noResultsText`          | `no-results-text`          | Empty-state text when the typed filter matches nothing.                                                                                                                                                                                    | `string`                                                                                                                 | `'No results'`                    |
| `open`                   | `open`                     | Open state.                                                                                                                                                                                                                                | `boolean`                                                                                                                | `false`                           |
| `options`                | `options`                  | Programmatic options (shared select model; `description` is accepted as a legacy alias for `supportingText`). Slotted `<md-select-option>` children take precedence when present. Also accepts a **JSON array string** as an attribute — `options='[{"value":"a","label":"Apple"}]'` — so a plain-HTML page needs no script; malformed JSON degrades to an empty list rather than throwing. | `MdAutocompleteOption[] \| string`                                                                                       | `[]`                              |
| `placeholder`            | `placeholder`              | Placeholder for the input.                                                                                                                                                                                                                 | `string`                                                                                                                 | `''`                              |
| `placement`              | `placement`                | Menu placement.                                                                                                                                                                                                                            | `"bottom-end" \| "bottom-start" \| "top-end" \| "top-start"`                                                             | `'bottom-start'`                  |
| `required`               | `required`                 | Required for native form parity.                                                                                                                                                                                                           | `boolean`                                                                                                                | `false`                           |
| `reserveSupportingSpace` | `reserve-supporting-space` | Always occupy the supporting-text line, even when there is no message, so a validation error does not push the content below it down. Forwarded to the embedded md-text-field. See that component for why it is opt-in.                    | `boolean`                                                                                                                | `false`                           |
| `rowHeight`              | `row-height`               | Fixed virtualized row height (px). Auto-measured from the first row if unset.                                                                                                                                                              | `number \| undefined`                                                                                                    | `undefined`                       |
| `softDisabled`           | `soft-disabled`            | Soft-disabled — focusable but non-interactive.                                                                                                                                                                                             | `boolean`                                                                                                                | `false`                           |
| `statusTemplate`         | `status-template`          | Template for the polite screen-reader status while the popup is open (localisable). `{count}` is replaced with the visible option count.                                                                                                   | `string`                                                                                                                 | `'{count} suggestions available'` |
| `supportingText`         | `supporting-text`          | Supporting / helper text below the field.                                                                                                                                                                                                  | `string`                                                                                                                 | `''`                              |
| `value`                  | `value`                    | Single-mode value — option `value` or free-solo string. Multi-mode value — array of option `value`s.                                                                                                                                       | `string \| string[]`                                                                                                     | `''`                              |
| `valueMissingLabel`      | `value-missing-label`      | Localized constraint-validation message shown when `required` is unmet. `errorText` still wins when set, so an app-supplied inline message and the native bubble stay in agreement.                                                        | `string`                                                                                                                 | `'Please make a selection'`       |
| `variant`                | `variant`                  | Visual variant of the inner text-field.                                                                                                                                                                                                    | `"filled" \| "outlined"`                                                                                                 | `'filled'`                        |
| `virtualize`             | `virtualize`               | Virtualization strategy (same engine as `md-select`):   - `'auto'` (default): virtualize above 200 rows.   - `'always'`: always virtualize the `options` / `loadOptions` dataset.   - `'never'`: plain DOM rendering.                      | `"always" \| "auto" \| "never"`                                                                                          | `'auto'`                          |


## Events

| Event              | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | Type                                                                                          |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------- |
| `mdChange`         | Fires whenever the committed selection changes.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | `CustomEvent<string \| string[]>`                                                             |
| `mdClear`          | Fires when the user clears the value.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    | `CustomEvent<void>`                                                                           |
| `mdClose`          | Fires when the menu closes.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | `CustomEvent<void>`                                                                           |
| `mdInput`          | Fires whenever the live input string changes.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | `CustomEvent<string>`                                                                         |
| `mdOpen`           | Fires when the menu 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 API, matching md-text-field and the native contract.
Validity was already published to the form here, but with no public method
a consumer could not ASK this control whether it was valid.

#### Returns

Type: `Promise<boolean>`



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

Close the menu.

#### Returns

Type: `Promise<void>`



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

Programmatically focus the input.

#### Returns

Type: `Promise<void>`



### `getLabels(values: string[]) => Promise<Record<string, string>>`

Resolve labels for a set of values (virtual-safe).

#### Parameters

| Name     | Type       | Description |
| -------- | ---------- | ----------- |
| `values` | `string[]` |             |

#### Returns

Type: `Promise<Record<string, string>>`



### `getValidity() => Promise<{ valid: boolean; validationMessage: string; flags: Record<string, boolean>; }>`

Current validity: boolean, message and flags. Mirrors md-text-field.

#### Returns

Type: `Promise<{ valid: boolean; validationMessage: string; flags: Record<string, boolean>; }>`



### `loadOptions(source: SelectOptionInit[] | OptionRowSource) => Promise<void>`

Load a large dataset into the WASM-backed virtualized list (rows are
byte-packed off the JS heap). Accepts an array or a `{ count, getRow }`
factory. Falls back to plain DOM when `virtualize="never"` or WASM is
unavailable.

#### Parameters

| Name     | Type                                    | Description |
| -------- | --------------------------------------- | ----------- |
| `source` | `OptionRowSource \| SelectOptionInit[]` |             |

#### Returns

Type: `Promise<void>`



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



#### Returns

Type: `Promise<boolean>`



### `setCustomValidity(message: string) => Promise<void>`



#### Parameters

| Name      | Type     | Description |
| --------- | -------- | ----------- |
| `message` | `string` |             |

#### Returns

Type: `Promise<void>`



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

Open the menu.

#### Returns

Type: `Promise<void>`




## Shadow Parts

| Part                 | Description |
| -------------------- | ----------- |
| `"caret"`            |             |
| `"chip"`             |             |
| `"chips"`            |             |
| `"clear"`            |             |
| `"field"`            |             |
| `"loading"`          |             |
| `"loading-progress"` |             |
| `"loading-spinner"`  |             |
| `"menu"`             |             |
| `"option-icon"`      |             |


## Dependencies

### Depends on

- [md-chip](../md-chip)
- [md-icon-button](../md-icon-button)
- [md-text-field](../md-text-field)
- [md-loading-indicator](../md-loading-indicator)
- [md-menu-item](../md-menu-item)
- [md-menu](../md-menu)
- [md-progress-indicator](../md-progress-indicator)

### Graph
```mermaid
graph TD;
  md-autocomplete --> md-chip
  md-autocomplete --> md-icon-button
  md-autocomplete --> md-text-field
  md-autocomplete --> md-loading-indicator
  md-autocomplete --> md-menu-item
  md-autocomplete --> md-menu
  md-autocomplete --> md-progress-indicator
  md-chip --> md-ripple
  md-icon-button --> md-ripple
  md-menu-item --> md-ripple
  style md-autocomplete fill:#f9f,stroke:#333,stroke-width:4px
```

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

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

AWC UI Operator’s Manual

# main-llm.md — AWC UI build director

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

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

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

---

## §1 — Match the task

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

### Optional discovery checklist

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

### 1.1 Scope and shape

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

### 1.2 Look and feel

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

### 1.3 Internationalization

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

### 1.4 Data and forms

15. **Is there significant tabular data?** How many rows, and is it
    server-paged? (Drives how you page `md-table` — it holds the state, you
    supply each page of rows.)
16. **Are there charts?** Which questions should they answer?
17. **How heavy are the forms?** Validation rules, async validation, multi-step?
18. **Rich text editing anywhere?** — ⚠️ **AWC UI has no RTE component.** If yes,
    the feature needs a third-party editor (TipTap, Lexical, Quill) styled with
    MD3 tokens. Resolve that choice if the project or request does not cover it.

### 1.5 Constraints

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

---

## §2 — Map answers to configuration

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

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

### 2.1 Global switches — the complete set

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

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

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

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

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

---

## §3 — Install and bootstrap

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

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

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

defineCustomElements(window);
```

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

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

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

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

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

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

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

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

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

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

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

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

---

## §4 — Global configuration reference

### 4.1 The token system

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

#### Color roles

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

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

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

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

#### Shape tokens

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

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

#### Elevation tokens

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

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

#### Motion tokens

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

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

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

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

#### State-layer opacities

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

#### Typography, spacing and layering

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

### 4.2 Theming and rebranding

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

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

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

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

### 4.3 Density

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

Two details that bite:

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

### 4.4 Dark mode

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

### 4.5 RTL

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

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

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

### 4.6 Internationalization

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

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

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

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

### 4.7 Forms and validation

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

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

Rules:

- Use a real `<form>`. `md-button type="submit"` calls `form.requestSubmit()`
  (not `submit()`), so the `submit` event fires **and** built-in constraint
  validation runs. `required` genuinely blocks submit.
  `md-button type="reset"` calls `form.reset()`.
- Give every control a `name`, or it will not appear in `FormData`.
- Do **not** add hidden `<input>`s to mirror values — that's the old pattern and
  it double-submits.
- Validity changes are announced on a `mdValidityChange` event on the control.
  Invalid submissions show the platform's message in the control's own inline
  error styling, with no browser validation popover. Correcting or resetting a
  field clears its generated message. `error` + `error-text` remain available
  for app-provided messages and take precedence over generated messages.
  `checkValidity()` checks silently; `reportValidity()` shows inline errors and
  focuses the first invalid control. Core submit buttons and Enter do the same.
- Boolean state props differ by control — `md-checkbox` uses `checked`,
  `md-switch` uses **`selected`**, `md-select-option` uses `selected`. Check the
  manual; guessing `checked` on a switch silently does nothing.

---

## §5 — Component decision matrix

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

### 5.1 Actions

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

### 5.2 Text input

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

### 5.3 Selection

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

### 5.4 Navigation

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

### 5.5 Containment and feedback

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

### 5.6 Data

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

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

### 5.7 Choosing the variant

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

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

### 5.8 Not in the library

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

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

---

## §6 — Component inventory

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

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

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

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

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

The library has 56 components and 25 sub-components, with 81 manuals in total.
Sub-components are only valid inside a parent (a table cell, a tab panel, a
select option). Every sub-component manual names its parent in the first line,
and §7 below summarises the nesting.

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

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

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

---

## §7 — Composition rules

### 7.1 What nests inside what

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

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

### 7.2 Pairs that belong together

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

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

### 7.3 Reusable interaction patterns

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

### 7.4 Nesting that is always wrong

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

---

## §8 — Page recipes

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

### 8.1 Login screen

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

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

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

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

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

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

### 8.2 Settings page (mobile)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

### 8.4 Dashboard with a FAB

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

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

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

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

### 8.5 Destructive confirmation dialog

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

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

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

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

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

### 8.6 Tabbed content page

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

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

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

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

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

### 8.7 The full recipe library

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

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

---

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

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

### 9.1 API and styling

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

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

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

### 9.2 Content and hierarchy

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

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

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

### 9.3 Accessibility contract

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

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

---

## §10 — Before you ship

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

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


## MCP server and reusable skills

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

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