Skip to content

Text Field

The library’s single text-entry primitive. Filled and outlined, single-line or auto-growing, with built-in clear / password-toggle / speech affordances, input masking via formatter / parser, debounced search, and the full constraint-validation API — form-associated via ElementInternals.

Live preview 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-text-field variant="filled" label="Email" type="email" supporting-text="We'll never share it" style="min-width: 240px;"></md-text-field>
<md-text-field variant="outlined" label="Email" type="email" supporting-text="We'll never share it" style="min-width: 240px;"></md-text-field>

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


<md-text-field></md-text-field>
  • Any free-text or numeric entry: names, emails, passwords, amounts, comments.
  • Multi-line entry (multiline="auto-grow" or "fixed").
  • As the input surface inside a composite you’re building.
SituationUse instead
Entry with a suggestion listmd-autocomplete
App-wide search with a results surfacemd-search
Choosing from a fixed setmd-select / md-multi-select
A date or a timemd-date-picker / md-time-picker
A bounded numeric range the user should feelmd-slider
A colourmd-color-picker
Rich textNot in this library — integrate a third-party editor
A yes / nomd-checkbox / md-switch
VariantContainerUse for
filledTonal fill + bottom active indicatorDefault. Dense forms, surfaces that need the field to read as a distinct target
outlinedTransparent, notched outlineSparse layouts, forms over patterned or coloured surfaces
Filled and outlined
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-text-field variant="filled" label="Filled" value="Jane Doe" style="min-width: 240px;"></md-text-field>
<md-text-field variant="outlined" label="Outlined" value="Jane Doe" style="min-width: 240px;"></md-text-field>

Icons are slots, not props: leading-icon and trailing-icon take any glyph — a Material Symbol, an SVG, an emoji.

Leading and trailing icons
search mail cancel
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-text-field variant="filled" label="Search" style="min-width: 240px;">
  <span slot="leading-icon" class="material-symbols-outlined">search</span>
</md-text-field>

<md-text-field variant="outlined" label="Email" style="min-width: 240px;">
  <span slot="leading-icon" class="material-symbols-outlined">mail</span>
  <span slot="trailing-icon" class="material-symbols-outlined">cancel</span>
</md-text-field>

prefix-text and suffix-text sit inside the field; max-length renders a counter.

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

<md-text-field variant="outlined" label="Amount" prefix-text="$" inputmode="decimal" style="min-width: 200px;"></md-text-field>
<md-text-field variant="outlined" label="Weight" suffix-text="kg" type="number" style="min-width: 200px;"></md-text-field>
<md-text-field variant="filled" label="Headline" max-length="60" value="Shipping update" style="min-width: 240px;"></md-text-field>
StateFocusableUse when
DefaultYesNormal entry
error + error-textYesThe value failed validation — set both
readonlyYesThe value is shown for reading and copying, not editing
disabledNoThe control is unavailable in this context

Without reserve-supporting-space the message line only exists when there’s a message, so content below shifts when an error appears. Turn it on in dense forms.

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

<md-text-field variant="outlined" label="Email" supporting-text="We'll never share it" style="min-width: 220px;"></md-text-field>
<md-text-field variant="outlined" label="Email" value="not-an-email" error error-text="Enter a valid email address" style="min-width: 220px;"></md-text-field>
<md-text-field variant="outlined" label="Read only" value="Locked value" readonly style="min-width: 220px;"></md-text-field>
<md-text-field variant="outlined" label="Disabled" value="Unavailable" disabled style="min-width: 220px;"></md-text-field>

readonly stays focusable and readable; disabled does not.

Affordances: clear, password toggle, speech

Section titled “Affordances: clear, password toggle, speech”
PropValuesUse for
clearable"internal" / "external" / falseA clear icon button. internal empties value itself; external only emits mdClear and leaves the value to you
password-toggle"internal" / "external" / falseA visibility toggle. internal flips the input type itself; external only emits mdPasswordToggle and leaves the type to you
speech-to-text"internal" / "external" / falseA mic button. internal drives the browser’s SpeechRecognition and appends the transcript; external only emits mdSpeechResult, so you can run your own engine

Writing the bare attribute — clearable, password-toggle or speech-to-text with no value — is normalised to "internal" on load, so it is a supported shorthand rather than a silent off.

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

<md-text-field variant="outlined" label="Search" type="search" clearable="internal" value="query" style="min-width: 220px;"></md-text-field>
<md-text-field variant="outlined" label="Password" type="password" password-toggle="internal" value="hunter2" style="min-width: 220px;"></md-text-field>
<md-text-field variant="outlined" label="Notes" speech-to-text="internal" speech-lang="en-US" style="min-width: 220px;"></md-text-field>

Speech-to-text depends on the browser’s SpeechRecognition API — feature-detect before advertising it.

Every built-in glyph is a slot, and slotting your own replaces the default:

clear-icon, password-toggle-icon and error-icon slots
backspace key report
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-text-field variant="outlined" label="Custom clear glyph" clearable="internal" value="query" style="min-width: 220px;">
  <span slot="clear-icon" class="material-symbols-outlined">backspace</span>
</md-text-field>
<md-text-field variant="outlined" label="Custom toggle glyph" type="password" password-toggle="internal" value="hunter2" style="min-width: 220px;">
  <span slot="password-toggle-icon" class="material-symbols-outlined">key</span>
</md-text-field>
<md-text-field variant="outlined" label="Custom error glyph" value="not-an-email" error error-text="Enter a valid email address" style="min-width: 220px;">
  <span slot="error-icon" class="material-symbols-outlined">report</span>
</md-text-field>

The speech-icon slot works the same way. chips is composite-owned — see the Internal props note under the API Reference.

multiline="auto-grow" expands as the user types; "fixed" keeps a fixed rows height.

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

<md-text-field variant="outlined" label="Notes" multiline="auto-grow" rows="2" max-length="500" style="min-width: 280px;"></md-text-field>
<md-text-field variant="filled" label="Comment" multiline="fixed" rows="4" style="min-width: 280px;"></md-text-field>

Use type, inputmode and autocomplete accurately — type="text" for an email address is a real accessibility and UX cost.

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

<md-text-field variant="outlined" label="Email" type="email" inputmode="email" autocomplete="email" style="min-width: 200px;"></md-text-field>
<md-text-field variant="outlined" label="Phone" type="tel" inputmode="tel" autocomplete="tel" style="min-width: 200px;"></md-text-field>
<md-text-field variant="outlined" label="Quantity" type="number" min="1" max="99" step="1" style="min-width: 200px;"></md-text-field>
<md-text-field variant="outlined" label="Website" type="url" inputmode="url" style="min-width: 200px;"></md-text-field>

Use type="tel" with inputmode="tel" for phone numbers — type="number" brings spinners and locale parsing that get in the way.

formatter and parser are function props — assign them in JS, they can’t cross the attribute boundary. They are a pair: formatter renders the display value, parser converts it back.

field.formatter = (raw) => new Intl.NumberFormat('en-US').format(Number(raw));
field.parser = (shown) => shown.replace(/,/g, '');
formatter / parser — the display is grouped, the value stays raw Open in Storybook
Focus the field to edit the raw value, blur to see it re-formatted. The event detail is always the parsed value.
Show code for each technology
<md-text-field id="amount" variant="outlined" label="Amount"
             prefix-text="$" inputmode="decimal" value="1234567"></md-text-field>

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

field.formatter = (raw) => (raw ? new Intl.NumberFormat('en-US').format(Number(raw)) : '');
field.parser    = (shown) => shown.replace(/,/g, '');

field.addEventListener('mdChange', (e) => save(Number(e.detail)));
</script>

restrict blocks disallowed characters as they are typed. It takes a regex character class ("[0-9]", "[0-9+() -]") or one of five named presets — numeric, integer, decimal, alpha, alphanumeric.

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

<md-text-field variant="outlined" label="Digits only" restrict="[0-9]" inputmode="numeric" style="min-width: 180px;"></md-text-field>
<md-text-field variant="outlined" label="numeric" restrict="numeric" inputmode="numeric" style="min-width: 180px;"></md-text-field>
<md-text-field variant="outlined" label="decimal" restrict="decimal" inputmode="decimal" style="min-width: 180px;"></md-text-field>
<md-text-field variant="outlined" label="alphanumeric" restrict="alphanumeric" style="min-width: 180px;"></md-text-field>

debounce and throttle gate mdSearch — not mdInput. mdInput always fires on every keystroke; mdSearch is the rate-limited twin you point at an API call. The usual search-as-you-type setup is debounce="300". Clearing the field re-emits mdSearch with an empty string, so stale results get dropped.

Debounced search field
search
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-text-field variant="outlined" label="Search" type="search" clearable="internal" debounce="300" style="min-width: 260px;">
  <span slot="leading-icon" class="material-symbols-outlined">search</span>
</md-text-field>
EventVetoableDetailFires
mdInputnostringOn every keystroke — never debounced or throttled
mdChangenostringOn commit (blur / Enter), and when the field is cleared
mdSearchnostringSame trigger as mdInput, but gated by debounce / throttle, de-duplicated against the last emitted value, and re-emitted as '' on clear
mdClearnovoidThe clear affordance was used (icon or Escape)
mdPasswordToggleno{ visible }Password visibility toggled
mdSpeechResultno{ transcript, listening }Speech recognition update
mdValidityChangeno{ valid, validationMessage, flags }Validity changed — not composed

These are notifications, not hooks — preventDefault() on any of them is a no-op (unlike md-button’s mdClick). Cancel at the source instead: restrict to block characters, or setCustomValidity() to reject a value.

mdInput fires per keystroke, mdSearch after the debounce
search Type, then pause. mdInput fires per keystroke; mdSearch waits 300ms.
Show code for each technology
<md-text-field id="search" label="Search" type="search"
             clearable="internal" debounce="300"></md-text-field>

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

field.addEventListener('mdInput',  (e) => setQuery(e.detail));      // every keystroke
field.addEventListener('mdSearch', (e) => fetchResults(e.detail));  // debounced 300ms
field.addEventListener('mdClear',  () => fetchResults(''));
field.addEventListener('mdValidityChange', (e) => {
  console.log(e.detail.valid, e.detail.validationMessage);
});
</script>

md-text-field is form-associated via ElementInternals, so name puts the value into FormData across the shadow boundary — no hidden <input> needed. required, pattern, min-length, max-length, min, max and step all feed native constraint validation, and setCustomValidity() covers server-side errors.

Form association — submit runs native validation
Save Press Save with an empty field — native constraint validation blocks the submit.
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<form id="profile">
  <md-text-field
    label="Email" type="email" name="email" required
    autocomplete="email" inputmode="email"
    supporting-text="We'll never share it"
    error-text="Enter a valid email address"
    reserve-supporting-space
    ></md-text-field>

    <md-text-field
      label="Display name" name="name" required
      min-length="2" max-length="32"
      supporting-text="2 to 32 characters"
      reserve-supporting-space
      ></md-text-field>

      <md-button variant="filled" type="submit">Save</md-button>
    </form>
<form id="profile">
<md-text-field
  label="Email" type="email" name="email" required
  autocomplete="email" inputmode="email"
  supporting-text="We'll never share it"
  error-text="Enter a valid email address"
  reserve-supporting-space
></md-text-field>

<md-text-field
  label="Display name" name="name" required
  min-length="2" max-length="32"
  supporting-text="2 to 32 characters"
  reserve-supporting-space
></md-text-field>

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

<script type="module">
const form  = document.getElementById('profile');
const field = form.querySelector('md-text-field');

form.addEventListener('submit', (e) => {
  e.preventDefault();
  console.log(Object.fromEntries(new FormData(e.target)));   // { email: '…', name: '…' }
});

// Server-side validation
field.setCustomValidity('That email is already registered');
// clear it again with: field.setCustomValidity('')
</script>

getValidity(), checkValidity() and reportValidity() mirror the native API; setFocus() and select() handle focus and selection.

Properties

PropertyAttributeTypeDefaultReflects
reserveSupportingSpacereserve-supporting-spacebooleanfalse
variantvariant'filled' | 'outlined''filled'
labellabelstring''
valuevaluestring''
placeholderplaceholderstring''
typetypestring'text'
namenamestring''Yes
patternpatternstring''
minLengthmin-lengthnumber | undefined
minminstring | number | undefined
maxmaxstring | number | undefined
stepstepstring | number | undefined
autocompleteautocompletestring''
inputModeinputmodestring''
enterKeyHintenterkeyhintstring''
autoCapitalizeautocapitalizestring''
spellcheckspellcheckboolean | undefined
disableddisabledbooleanfalseYes
readOnlyreadonlybooleanfalse
requiredrequiredbooleanfalse
errorerrorbooleanfalse
errorTexterror-textstring''
supportingTextsupporting-textstring''
prefixTextprefix-textstring''
suffixTextsuffix-textstring''
maxLengthmax-lengthnumber | undefined
clearableclearable'internal' | 'external' | falsefalse
passwordTogglepassword-toggle'internal' | 'external' | falsefalse
densitydensity0 | -1 | -2 | -3 | -40Yes
multilinemultiline'auto-grow' | 'fixed' | falsefalse
rowsrowsnumber | undefined
speechToTextspeech-to-text'internal' | 'external' | falsefalse
speechLangspeech-langstring''
restrictrestrictstring''
formatterJS only(value: string) => string
parserJS only(displayValue: string) => string
formatOnformat-on'blur' | 'input''blur'
focusBorderWidthfocus-border-widthnumber3Yes
appearFocusedappear-focusedbooleanfalseYes
chipsWrapchips-wrapbooleanfalseYes
debouncedebouncenumber0Yes
throttlethrottlenumber0Yes
inputRoleinput-role'' | 'combobox'''
inputExpandedinput-expandedbooleanfalse
inputAriaAutocompleteinput-aria-autocomplete'' | 'none' | 'inline' | 'list' | 'both'''

Methods

MethodParameters
setFocus()none
select()none
getInputElement()none
getValidity()none
setCustomValidity()message: string
checkValidity()none
reportValidity()none

Slots

SlotDescription
leading-iconCustom leading icon (overrides nothing — pure decoration)
chips
clear-iconCustom clear-button glyph (replaces the default "cancel" icon)
password-toggle-iconCustom password-toggle glyph
speech-iconCustom microphone glyph
error-iconCustom error-state icon (replaces the default filled "error")
trailing-iconCustom trailing icon

CSS Custom Properties

Override on the host element for per-instance theming:

PropertyDescription
--md-text-field-container-colorFilled-variant container background
--md-text-field-input-colorInput text colour
--md-text-field-label-colorResting label colour
--md-text-field-active-indicator-colorFilled bottom line + outlined border on focus
--md-text-field-outline-colorOutlined-variant resting border
--md-text-field-error-colorError accent (label, indicator, icon, helper)
--md-text-field-supporting-text-colorSupporting text + character counter
--md-text-field-icon-colorLeading / trailing / clear / password / speech icons
--md-text-field-icon-sizeAll glyph sizes (24px; tapers with
--md-text-field-optical-nudgeOutlined only: px the input text is
--md-text-field-container-shapeBorder-radius for both variants
--md-text-field-filled-pillFilled only: 1 rounds all corners (pill);
--md-text-field-cursorCursor over the field (default: text)
--md-text-field-focus-border-widthBorder thickness on focus (1, 2, or 3 px)
--md-text-field-focus-insetInset of the focus ring (default 0 — it
--md-text-field-padding-inline-start
--md-text-field-padding-inline-end

CSS Shadow Parts

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

PartDescription
inputThe native input / textarea
containerField container (state layer + input row)
labelFloating label
chips
supporting-textSupporting / error text line
counterCharacter counter
  • label provides the accessible name — always set it. aria-label works too, but a visible label is better.
  • Errors are announced through the supporting line; keep messages specific.
  • readonly stays focusable and readable; disabled does not.
  • The focus indicator is a 3px border by default (--md-text-field-focus-border-width), and appear-focused can force the focused look for composite triggers.
  • The counter (max-length) is exposed alongside the field, not only visually.
  • The clear, password-toggle and speech affordances are real buttons — each is reachable by keyboard, in DOM order after the input.
  • Escape clears a clearable="internal" field and stops there, so an enclosing <dialog> does not also close.
  • Verified against axe-core with zero WCAG 2.1 AA violations.

Tab through these: the read-only field takes focus and can be copied from, the disabled one is skipped, the error field announces its message, and the last has no visible label so it leans entirely on aria-label.

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

<md-text-field variant="outlined" label="Editable" value="Ada Lovelace" style="min-width: 220px;"></md-text-field>
<md-text-field variant="outlined" label="Read only — still focusable" value="AWC-4021" readonly style="min-width: 220px;"></md-text-field>
<md-text-field variant="outlined" label="Disabled — skipped" value="Unavailable" disabled style="min-width: 220px;"></md-text-field>
<md-text-field variant="outlined" label="Email" type="email" value="not-an-email" required error error-text="Enter a valid email address" style="min-width: 220px;"></md-text-field>
<md-text-field variant="outlined" placeholder="Filter invoices" aria-label="Filter invoices" clearable="internal" value="4021" style="min-width: 220px;"></md-text-field>

The last field has no visible label, so aria-label is the only accessible name it has — prefer a visible label wherever the layout allows one. Note that its clear button is its own tab stop, and that Escape clears the field without leaving it.

RTL — layout, prefix / suffix and icon sides mirror automatically. Set the direction on an ancestor and the whole field follows; nothing on the component needs changing. See RTL.

<div dir="rtl">
<md-text-field label="بحث" clearable="internal"></md-text-field>
</div>
Same markup, dir=ltr vs dir=rtl
ltr
search
rtl
search
Show code for each technology
<!-- index.html <head> — the icon font the components draw from -->
<link rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">

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

<div style="display:grid;grid-template-columns:auto 1fr;gap:14px 16px;align-items:center;">
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">ltr</span>
  <div dir="ltr" style="display:flex;gap:12px;flex-wrap:wrap;align-items:center;">
    <md-text-field variant="outlined" label="Search" value="Hello world" clearable="internal" style="min-width:200px;"><span slot="leading-icon" class="material-symbols-outlined">search</span></md-text-field>
    <md-text-field variant="outlined" label="Amount" prefix-text="$" suffix-text="USD" value="1200" inputmode="decimal" style="min-width:200px;"></md-text-field>
  </div>
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">rtl</span>
  <div dir="rtl" style="display:flex;gap:12px;flex-wrap:wrap;align-items:center;">
    <md-text-field variant="outlined" label="بحث" value="مرحبا بالعالم" clearable="internal" style="min-width:200px;"><span slot="leading-icon" class="material-symbols-outlined">search</span></md-text-field>
    <md-text-field variant="outlined" label="المبلغ" prefix-text="$" suffix-text="USD" value="1200" inputmode="decimal" style="min-width:200px;"></md-text-field>
  </div>
</div>

prefix-text and suffix-text swap sides with the direction — check that a currency symbol still lands where the locale expects it, because a $ that belongs on the leading side in English belongs there in Arabic too, and the mirror will move it.

density="-1…-4" compresses the container from 56px down to 40px, 4px per rung, and the label, input line box and every glyph taper with it.

Density 0 through -4 — height, padding, label and glyphs all taper
0
-1
-2
-3
-4
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<div style="display:grid;grid-template-columns:auto 1fr;gap:14px 16px;align-items:center;">
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">0</span>
  <div style="display:flex;gap:12px;flex-wrap:wrap;align-items:center;">
    <md-text-field variant="filled" label="Email" density="0" value="ada@example.com" style="min-width:200px;"></md-text-field>
    <md-text-field variant="outlined" label="Email" density="0" value="ada@example.com" style="min-width:200px;"></md-text-field>
  </div>
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">-1</span>
  <div style="display:flex;gap:12px;flex-wrap:wrap;align-items:center;">
    <md-text-field variant="filled" label="Email" density="-1" value="ada@example.com" style="min-width:200px;"></md-text-field>
    <md-text-field variant="outlined" label="Email" density="-1" value="ada@example.com" style="min-width:200px;"></md-text-field>
  </div>
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">-2</span>
  <div style="display:flex;gap:12px;flex-wrap:wrap;align-items:center;">
    <md-text-field variant="filled" label="Email" density="-2" value="ada@example.com" style="min-width:200px;"></md-text-field>
    <md-text-field variant="outlined" label="Email" density="-2" value="ada@example.com" style="min-width:200px;"></md-text-field>
  </div>
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">-3</span>
  <div style="display:flex;gap:12px;flex-wrap:wrap;align-items:center;">
    <md-text-field variant="filled" label="Email" density="-3" value="ada@example.com" style="min-width:200px;"></md-text-field>
    <md-text-field variant="outlined" label="Email" density="-3" value="ada@example.com" style="min-width:200px;"></md-text-field>
  </div>
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">-4</span>
  <div style="display:flex;gap:12px;flex-wrap:wrap;align-items:center;">
    <md-text-field variant="filled" label="Email" density="-4" value="ada@example.com" style="min-width:200px;"></md-text-field>
    <md-text-field variant="outlined" label="Email" density="-4" value="ada@example.com" style="min-width:200px;"></md-text-field>
  </div>
</div>

The density prop and a global data-density ancestor drive the same signal — --md-sys-density-scale — so a wrapper sets the rung for everything inside it and any one field can override it with a rung of its own.

dir=rtl with data-density=-2: two fields inherit, one tightens to -4, one resets the signal to 0
mail
Show code for each technology
<!-- index.html <head> — the icon font the components draw from -->
<link rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">

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

<div dir="rtl" data-density="-2" style="display:flex;gap:12px;flex-wrap:wrap;align-items:center;">
  <md-text-field variant="outlined" label="البريد الإلكتروني" value="ada@example.com" style="min-width:200px;"><span slot="leading-icon" class="material-symbols-outlined">mail</span></md-text-field>
  <md-text-field variant="outlined" label="كلمة المرور" type="password" password-toggle="internal" value="hunter2" style="min-width:200px;"></md-text-field>
  <md-text-field variant="outlined" label="أكثر إحكاما" density="-4" value="مرحبا" style="min-width:200px;"></md-text-field>
  <md-text-field variant="outlined" label="افتراضي" value="مرحبا" style="--md-sys-density-scale: 0; min-width:200px;"></md-text-field>
</div>

Density — reach for a rung, not a hand-tuned height: the rung tapers the container, the padding, the label and the glyphs together, which a height override cannot. See Density.

i18n — translate label, supporting-text, error-text, placeholder, prefix-text and suffix-text. Set speech-lang per locale, and use Intl inside formatter for locale-correct numbers and currency.

Custom propertyPurposeDefault
--md-text-field-container-colorFilled-variant container background--md-sys-color-surface-container-highest
--md-text-field-input-colorInput text and caret colour--md-sys-color-on-surface
--md-text-field-label-colorResting label colour--md-sys-color-on-surface-variant
--md-text-field-active-indicator-colorFilled underline, outlined border on focus--md-sys-color-primary
--md-text-field-outline-colorOutlined-variant resting border--md-sys-color-outline
--md-text-field-error-colorError accent (label, indicator, icon, helper)--md-sys-color-error
--md-text-field-supporting-text-colorSupporting text and character counter--md-sys-color-on-surface-variant
--md-text-field-icon-colorEvery glyph: leading, trailing, clear, password, speech--md-sys-color-on-surface-variant
--md-text-field-icon-sizeEvery glyph’s size; tapers 1px per density rung24px (18px floor)
--md-text-field-container-shapeCorner radius, both variants--md-sys-shape-corner-extra-small (4px)
--md-text-field-filled-pillFilled only: 1 rounds the bottom corners too (pill)0
--md-text-field-padding-inline-start / -endPer-side container inset--md-sys-spacing-inset-lg + 7px
--md-text-field-optical-nudgeOutlined only: px the input text is nudged down0px
--md-text-field-focus-border-widthBorder thickness on focus3px
--md-text-field-focus-insetNegative inset compensating for the focus border-2px
--md-text-field-cursorCursor over the fieldtext
Themed instances
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-text-field variant="outlined" label="Squared off" value="Custom" style="--md-text-field-container-shape: 0px; --md-text-field-outline-color: var(--md-sys-color-primary); min-width: 220px;"></md-text-field>
<md-text-field variant="filled" label="Pill" value="Custom" style="--md-text-field-filled-pill: 1; --md-text-field-container-shape: 28px; min-width: 220px;"></md-text-field>

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

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

<md-text-field label="Outlined" value="Ada Lovelace" style="min-width: 220px;"></md-text-field>
<md-text-field variant="filled" label="Filled" value="Ada Lovelace" style="min-width: 220px;"></md-text-field>
<md-text-field label="With error" value="not-an-email" error error-text="Enter a valid email" style="min-width: 220px;"></md-text-field>

CSS Partscontainer (the field box), input (the native <input> / <textarea>), label (the floating label), chips (the slotted chip region a composite fills), supporting-text (the message line) and counter (the character counter). The icons are slots, not parts — style them from your own light-DOM CSS.

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

<style>
  .parts-tf::part(container) { border-radius: 4px; }
  .parts-tf::part(label) { text-transform: uppercase; letter-spacing: .08em; font-size: 11px; }
  .parts-tf::part(supporting-text) { font-style: italic; }
  .parts-tf::part(counter) { font-weight: 700; }
</style>
<md-text-field class="parts-tf" label="Styled parts" value="Ada Lovelace" max-length="40" supporting-text="Styled supporting text" style="--md-text-field-label-color: var(--md-sys-color-primary); min-width: 260px;"></md-text-field>
md-text-field::part(container) { border-radius: 4px; }
md-text-field::part(supporting-text) { font-style: italic; }

md-autocomplete · md-search · md-select · md-multi-select · md-date-picker · md-time-picker · md-color-picker · md-icon-button

For AI Agents — md-text-field

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-text-field 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-text-field readme.md

# md-text-field

<!-- llm:meta
tag: md-text-field
category: text-input
status: md3-mapped
m3-guidelines: https://m3.material.io/components/text-fields/guidelines
form-associated: true
depends-on: none
used-by: md-autocomplete, md-date-picker, md-multi-select, md-number-field, md-select, md-time-picker, md-transfer-list
-->

**The library's single text-entry primitive.** Filled and outlined variants,
single-line or multiline, with optional clear / password-toggle / speech-to-text
affordances, display masking via `formatter`/`parser`, a debounced/throttled
search event, and the full constraint-validation API through ElementInternals.

> Setup, theming, density and i18n are configured once for the whole library —
> see [`main-llm.md`](../../../../../main-llm.md), shipped beside these manuals in
> the package's `docs/` folder.

---

## When to use

- **Any** free-text or numeric entry: names, emails, passwords, amounts,
  comments.
- Multi-line entry (`multiline="auto-grow"` or `multiline="fixed"`).
- As the input surface inside a composite you are building — it exposes
  `getInputElement()` and `input-role="combobox"` for exactly that.

## When NOT to use

| Situation | Use instead |
|---|---|
| Entry with a suggestion list | `md-autocomplete` |
| App-wide search with a results surface | `md-search` |
| Choosing from a fixed set | `md-select` / `md-multi-select` |
| A number with steppers and clamping | `md-number-field` |
| A one-time passcode split into boxes | `md-otp-field` |
| A date or a time | `md-date-picker` / `md-time-picker` |
| A bounded numeric range the user should feel | `md-slider` |
| A colour | `md-color-picker` |
| A yes/no | `md-checkbox` / `md-switch` |
| Rich text | Not in this library — integrate a third-party editor |

## Decision cues

| Need | Setting |
|---|---|
| Default look | `variant="filled"` |
| On a busy surface, or beside other outlined fields | `variant="outlined"` |
| Growing comment box | `multiline="auto-grow"` (2 rows by default) |
| Fixed-height, scrolling textarea | `multiline="fixed"` (4 rows by default) |
| A clear button that empties the field | `clearable="internal"` |
| A clear button you handle yourself | `clearable="external"` |
| Show/hide password | `password-toggle="internal"` with `type="password"` |
| Dictation | `speech-to-text="internal"` + `speech-lang` |
| Currency / phone masking | `formatter` + `parser` (+ `format-on`) |
| Block disallowed characters as typed | `restrict` |
| Avoid layout jump when an error appears | `reserve-supporting-space` |
| Search-as-you-type | `debounce="300"` + listen for `mdSearch` |
| Character counter | `max-length` |
| Keep the field looking focused while a popup is open | `appear-focused` |
| Build a combobox on top of this field | `input-role="combobox"` |

## API contract

```html
<md-text-field
  variant="filled|outlined"                  <!-- default: filled -->
  label="Email"
  value=""
  placeholder=""
  type="text"                                <!-- default: text; any input type -->
  name="email"
  required
  pattern=".+@.+"
  min-length="3" max-length="80"
  min="" max="" step=""
  autocomplete="email"
  inputmode="email"
  enterkeyhint="done"
  autocapitalize="none"
  spellcheck="false"
  supporting-text="We'll never share it"
  error error-text="Enter a valid email"
  reserve-supporting-space
  prefix-text="$" suffix-text="kg"
  clearable="internal|external"              <!-- default: off -->
  password-toggle="internal|external"        <!-- default: off -->
  speech-to-text="internal|external"         <!-- default: off -->
  speech-lang="en-US"
  multiline="auto-grow|fixed"                <!-- default: off (single line) -->
  rows="4"                                   <!-- default: 2 auto-grow, 4 fixed -->
  restrict="numeric|integer|decimal|alpha|alphanumeric|[0-9+() -]"
  format-on="blur|input"                     <!-- default: blur -->
  focus-border-width="3"                     <!-- default: 3 -->
  appear-focused
  chips-wrap
  debounce="0" throttle="0"                  <!-- default: 0 (both off) -->
  disabled readonly
  density="-1|-2|-3|-4"                      <!-- default: 0 (uncompacted) -->
  input-role="combobox"                      <!-- default: '' -->
  input-expanded
  input-aria-autocomplete="none|inline|list|both"
></md-text-field>
```

`formatter` and `parser` are **function props** — set them in JS, never as
attributes:

```js
const field = document.querySelector('md-text-field');
field.formatter = (raw) => new Intl.NumberFormat('en-US').format(Number(raw));
field.parser = (shown) => shown.replace(/,/g, '');
```

**Events**

| Event | Detail | Fires |
|---|---|---|
| `mdInput` | `string` | Every keystroke, and on clear / speech result. Never debounced |
| `mdChange` | `string` | On the inner input's native `change` (commit), on clear, and when speech recognition ends |
| `mdSearch` | `string` | Typing and clearing only, gated by `debounce` / `throttle` and skipped when the value has not changed since the last emit. Speech results do **not** raise it |
| `mdClear` | `void` | Clear button pressed, or Escape with `clearable="internal"` |
| `mdPasswordToggle` | `{ visible: boolean }` | Password visibility button pressed |
| `mdSpeechResult` | `{ transcript: string; listening: boolean }` | Speech recognition update |
| `mdValidityChange` | `{ valid, validationMessage, flags }` | Validity actually changed — `bubbles: true`, **`composed: false`** |

The host also re-dispatches a bubbling native `change` event, so form-level
`change` delegation works.

**Methods** — `setFocus()`, `select()`, `getInputElement()`, `getValidity()`,
`checkValidity()`, `reportValidity()`, `setCustomValidity(message)`.

**Slots** — `leading-icon`, `trailing-icon`, `chips`, `clear-icon`,
`password-toggle-icon`, `speech-icon`, `error-icon`.

**Parts** — `container`, `label`, `input`, `chips`, `supporting-text`, `counter`.

### Behavioral contract worth knowing

- **`error-text` replaces `supporting-text`** when `error` is set. Setting
  `error-text` alone does nothing — the field only switches to the error look,
  the error icon and the `role="alert"` message line when `error` is present.
- Without `reserve-supporting-space` the message row is rendered only when there
  is a message or a counter, so the first error pushes everything below it down.
- `clearable`, `password-toggle` and `speech-to-text` are **three-way enums**,
  not booleans. Writing them as a bare attribute (`clearable`) is read as
  `"internal"`. Any other string value (`clearable="true"`) shows the button but
  behaves as `"external"` — it emits `mdClear` and never empties the field.
- `value` is **not reflected** to an attribute, deliberately: typed values
  including passwords must not appear in the DOM.
- In single-line mode newlines are stripped from `value`, on initial render and
  on programmatic writes, matching a native `<input>`.
- Pressing **Enter** in a single-line field submits the owning form via
  `requestSubmit()`, activating that form's default submit button.
- Pressing **Escape** clears the field when `clearable="internal"`, the field has
  a value and is neither `readonly` nor `disabled`. The keypress is stopped, so a
  surrounding `<dialog>` does not also close on that press.
- `readonly` blocks the clear button and the mic; `disabled` blocks all three
  affordances. `disabled` also follows an ancestor `<form>`/`<fieldset>` being
  disabled.
- `formatter` renders the display string; `parser` converts it back. `value`,
  `mdInput`, `mdChange` and the submitted FormData always carry the **parsed**
  value. `format-on="input"` **requires** a `parser`; without one the component
  falls back to `"blur"` and warns in dev builds.
- With `format-on="blur"` (the default) the raw value is shown while focused and
  the formatted value while blurred.
- `restrict` accepts the presets `numeric`, `integer`, `decimal`, `alpha`,
  `alphanumeric`, or a character-class string such as `[0-9+() -]`. Disallowed
  characters are stripped as typed and the caret is preserved. With `restrict`
  set, `max-length` is enforced **after** stripping, so pasting
  `(555) 123-4567` into a numeric field keeps all ten digits.
- The counter shows `value.length / max-length`, except with a live formatter
  where it counts the value with `-`, `/` and whitespace removed.
- `mdValidityChange` does **not** fire on mount — the first evaluation only
  primes the baseline — and never fires twice for the same state.
- `mdValidityChange` is **not composed**, so it does not escape the shadow root
  of a composite that embeds this field. Listen on the `md-text-field` element
  itself, or on a `<form>`/app root in the same tree.
- `getValidity()` / `checkValidity()` report on the **submitted** value, not on
  the formatted display string.
- An `aria-label` set on the host is moved onto the inner input at load and
  removed from the host (a role-less host may not carry `aria-label`) — but
  **only when no `label` is set**. With a `label` present the host keeps its
  `aria-label` attribute and it is ignored: the `label` is the accessible name.
- `aria-haspopup="dialog"` and `aria-haspopup="listbox"` set on the host are
  mirrored onto the inner input. `aria-controls` is never mirrored — an IDREF
  cannot resolve across the shadow boundary.
- To build a combobox on this field, set `input-role="combobox"`. Only then are
  `input-expanded` and `input-aria-autocomplete` applied. Setting
  `role="combobox"` on the host instead does nothing useful.
- Dictation raises `mdInput` (and `mdChange` when recognition ends) but never
  `mdSearch`. With `speech-to-text` and a debounced search, run the fetch from
  `mdInput` as well, or a dictated query is silently dropped.
- `password-toggle` is ignored in multiline mode.
- IME composition is respected: `restrict`, `parser` and `formatter` run once on
  `compositionend`, not on every intermediate keystroke.

---

## Do / Don't

Sourced from [M3 · Text fields · Guidelines](https://m3.material.io/components/text-fields/guidelines).

| ✅ Do | ❌ Don't |
|---|---|
| Separate `filled` and `outlined` fields by region | Don't put both variants next to each other or in the same form |
| Keep the label short, clear and fully visible | Don't truncate label text |
| Keep labels to one line | Don't let label text wrap to multiple lines |
| **Swap** supporting text for error text | Don't try to show error text in addition to supporting text |
| Leave enough space between fields for a wrapped error | Don't let a multi-line error bump the layout — use `reserve-supporting-space` |
| Constrain field width on large screens | Don't let text fields span the full width of a large screen |
| Always provide a `label` | Don't rely on `placeholder` as the label |
| Use `type`/`inputmode`/`autocomplete` accurately | Don't ship `type="text"` for emails and numbers |

---

## Patterns

```html
<!-- Standard validated field inside a form.
     `error-text` renders only while `error` is set, so the app flips `error`. -->
<form id="signup">
  <md-text-field
    id="email"
    label="Email" type="email" name="email" required
    autocomplete="email" inputmode="email"
    supporting-text="We'll never share it"
    error-text="Enter a valid email address"
    reserve-supporting-space
  ></md-text-field>
  <md-button type="submit">Sign up</md-button>
</form>
<script type="module">
  const email = document.getElementById('email');
  // Validate at commit points, not on every keystroke.
  email.addEventListener('mdChange', async () => {
    email.error = !(await email.checkValidity());
  });
</script>
```

```html
<!-- Password with a visibility toggle -->
<md-text-field
  label="Password" type="password" name="password"
  password-toggle="internal" min-length="8" required
></md-text-field>
```

```html
<!-- Auto-growing comment box with a counter -->
<md-text-field
  label="Notes" multiline="auto-grow" rows="3" max-length="500"
></md-text-field>
```

```html
<!-- Search-as-you-type: mdInput is per-keystroke, mdSearch is debounced -->
<md-text-field
  id="q" label="Search" type="search" clearable="internal" debounce="300"
></md-text-field>
<script type="module">
  const field = document.getElementById('q');
  field.addEventListener('mdSearch', (e) => {
    console.log('debounced query:', e.detail);
  });
</script>
```

```html
<!-- Masked currency: value stays numeric, display is grouped -->
<md-text-field
  id="amount" label="Amount" prefix-text="$"
  inputmode="decimal" restrict="decimal"
></md-text-field>
<script type="module">
  const f = document.getElementById('amount');
  f.formatter = (raw) => (raw ? new Intl.NumberFormat('en-US').format(Number(raw)) : '');
  f.parser = (shown) => shown.replace(/,/g, '');
  f.addEventListener('mdChange', (e) => console.log(Number(e.detail))); // parsed
</script>
```

```html
<!-- Server-side validation -->
<md-text-field id="user" label="Username" name="username"></md-text-field>
<script type="module">
  const el = document.getElementById('user');
  el.addEventListener('mdValidityChange', (e) => console.log(e.detail.valid));
  await el.setCustomValidity('That username is already taken');
  // clear it again with:
  // await el.setCustomValidity('');
</script>
```

```html
<!-- Error state driven by the app -->
<md-text-field
  id="code" label="Invite code" error error-text="Code not recognised"
  reserve-supporting-space
></md-text-field>
```

## Anti-patterns

| ❌ Wrong | ✅ Right | Why |
|---|---|---|
| `placeholder` used instead of `label` | Set `label` | The placeholder is only rendered once the label has floated, and it is not an accessible name. |
| `error-text` set without `error` | Set both | `error-text` alone leaves the field in its normal state. |
| Showing supporting **and** error text | Swap one for the other | `error-text` replaces `supporting-text` — you cannot render both. |
| `clearable="true"` | `clearable="internal"` or bare `clearable` | Any value other than `internal` behaves as `external`: the button shows but never empties the field. |
| `formatter="..."` as an attribute | `el.formatter = fn` in JS | Function props do not cross the attribute boundary. |
| `format-on="input"` with only a `formatter` | Supply a `parser` too | Without a parser the component silently falls back to `format-on="blur"`. |
| Reading `value` expecting the formatted string | `value` is the **parsed** value | `formatter` affects display only. |
| Debouncing by listening to `mdInput` | Listen to `mdSearch` and set `debounce` | `debounce`/`throttle` gate `mdSearch` only; `mdInput` always fires per keystroke. |
| Listening for `mdValidityChange` on an ancestor shadow host | Listen on the `md-text-field` itself | It is `composed: false` by design. |
| `role="combobox"` on the host | `input-role="combobox"` | The role must land on the real inner textbox; a host attribute never reaches it. |
| A hidden `<input>` to get the value into `FormData` | Give the field a `name` | It is already form-associated via ElementInternals. |
| `type="number"` for phone numbers | `type="tel"` + `inputmode="tel"` | Spinners and locale parsing get in the way. |
| Mixing `filled` and `outlined` in one form | Pick one per region | M3 explicit rule. |
| `density="0"` to escape an inherited rung | `style="--md-sys-density-scale: 0"` | There is no `density="0"` rule; rung 0 is the default and is inert. |

## Accessibility, RTL, density, i18n

**Accessibility** — `label` becomes the inner input's accessible name; always
set it. An `aria-label` on the host is relocated onto the input only when there
is no `label`; with a `label` present the host attribute stays put and is
ignored.
The supporting line carries `role="alert"` while `error` is set, so the message
is announced. `max-length` renders a counter that is wired in through
`aria-describedby`, as is the supporting text. The clear, password and mic
buttons carry their own English labels (`Clear`, `Show password` /
`Hide password`, `Start voice input` / `Stop listening`); replace their glyphs
through the `clear-icon`, `password-toggle-icon` and `speech-icon` slots.
`readonly` stays focusable; `disabled` does not. The host uses
`delegatesFocus`, so `element.focus()` and a click anywhere in the container
land in the input. `focus-border-width` (1, 2 or 3) sets the focused border
thickness — do not lower it below the contrast your design needs.

**RTL** — the container, prefix/suffix and icon sides mirror automatically from
the document's `dir`. Check that a currency prefix still lands where the locale
expects.

**Density** — set `density="-1"` … `density="-4"` for a local override, or let
the field inherit an ancestor's `data-density` rung. Both drive the same
`--md-sys-density-scale` signal; the local prop wins because it is set on the
host. There is no `density="0"` rule — rung 0 is the uncompacted default.

**i18n** — translate `label`, `placeholder`, `supporting-text`, `error-text`,
`prefix-text` and `suffix-text`. Set `speech-lang` per locale. Use `Intl`
inside `formatter` for locale-correct numbers and currency. Note that speech
input depends on the browser's Web Speech API and is unavailable in browsers
that do not implement it — the component logs a warning and does nothing.

## Related components

`md-autocomplete` · `md-search` · `md-number-field` · `md-otp-field` ·
`md-select` · `md-multi-select` · `md-date-picker` · `md-time-picker` ·
`md-color-picker` · `md-icon-button`

## Theming

| Custom property | Purpose | Default |
|---|---|---|
| `--md-text-field-container-color` | Filled container background | `--md-sys-color-surface-container-highest` |
| `--md-text-field-input-color` | Input text and caret colour | `--md-sys-color-on-surface` |
| `--md-text-field-label-color` | Label colour | `--md-sys-color-on-surface-variant` |
| `--md-text-field-active-indicator-color` | Filled underline | `--md-sys-color-primary` |
| `--md-text-field-outline-color` | Outlined border | `--md-sys-color-outline` |
| `--md-text-field-error-color` | Error state colour | `--md-sys-color-error` |
| `--md-text-field-supporting-text-color` | Supporting / counter line | `--md-sys-color-on-surface-variant` |
| `--md-text-field-icon-color` | Leading and trailing icon colour | `--md-sys-color-on-surface-variant` |
| `--md-text-field-icon-size` | Leading and trailing icon size | density-scaled, `24px` at rung 0 |
| `--md-text-field-container-shape` | Corner radius | `--md-sys-shape-corner-extra-small` (4px) |
| `--md-text-field-filled-pill` | Set to `1` for a pill-shaped filled variant | `0` |
| `--md-text-field-padding-inline-start` | Leading inline padding | `--md-sys-spacing-inset-lg` + 7px (23px at rung 0) |
| `--md-text-field-padding-inline-end` | Trailing inline padding | `--md-sys-spacing-inset-lg` + 7px (23px at rung 0) |
| `--md-text-field-focus-border-width` | Focused border / underline thickness | `3px` |
| `--md-text-field-focus-inset` | Focused outline inset | `-2px` |
| `--md-text-field-optical-nudge` | Vertical optical nudge of the outlined input | `0px` |
| `--md-text-field-cursor` | Cursor over the container and input | `text` |

**CSS parts** — `container`, `label`, `input`, `chips`, `supporting-text`,
`counter`.

```css
md-text-field {
  --md-text-field-container-shape: 12px;
  --md-text-field-focus-border-width: 2px;
}
md-text-field::part(input) {
  font-variant-numeric: tabular-nums;
}
```

<!-- Auto Generated Below -->


## Properties

| Property                 | Attribute                  | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | Type                                              | Default     |
| ------------------------ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- | ----------- |
| `appearFocused`          | `appear-focused`           | Force the field to render in its focused visual state regardless of the underlying input's actual focus. Use when the field acts as an *anchor* for a popover, dialog, or menu, and the OS focus has moved into that surface — for example the `md-time-picker` keeps its trigger field in the focused look while the picker modal is open, so the trigger and the modal read as one connected control.  Visual-only: affects the notched-outline width / colour, the floating label position, and the leading-icon colour through md-text-field's existing `.md-text-field--focused` rules. Does NOT alter formatter / blur-format behaviour (those keep tracking real focus events), so `formatOn="blur"` still flips between formatted and raw on real focus/blur even while `appear-focused` is on. | `boolean`                                         | `false`     |
| `autoCapitalize`         | `autocapitalize`           |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `string`                                          | `''`        |
| `autocomplete`           | `autocomplete`             | Input behavior passthrough (password managers / mobile keyboards).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `string`                                          | `''`        |
| `chipsWrap`              | `chips-wrap`               | When leading `chips` are slotted, allow them to WRAP onto multiple rows so the field grows to fit them. Default `false`: chips stay on a single line and the field keeps its normal height (the consumer, e.g. md-multi-select, is responsible for handling overflow — clipping or a "+N" counter).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | `boolean`                                         | `false`     |
| `clearable`              | `clearable`                | Show a clear icon button. 'internal' clears the value directly; 'external' emits mdClear and leaves value control to the consumer.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `"external" \| "internal" \| boolean`             | `false`     |
| `debounce`               | `debounce`                 | Debounce delay (ms) for the mdSearch event. Emits after the user stops typing for this duration. 0 = disabled.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | `number`                                          | `0`         |
| `density`                | `density`                  | Density scale: 0 (default 56px), -1 (52px), -2 (48px), -3 (44px).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       | `-1 \| -2 \| -3 \| -4 \| 0`                       | `0`         |
| `disabled`               | `disabled`                 |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `boolean`                                         | `false`     |
| `enterKeyHint`           | `enterkeyhint`             |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `string`                                          | `''`        |
| `error`                  | `error`                    |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `boolean`                                         | `false`     |
| `errorText`              | `error-text`               |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `string`                                          | `''`        |
| `focusBorderWidth`       | `focus-border-width`       | Border width when focused: 1, 2, or 3 pixels.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           | `number`                                          | `3`         |
| `formatOn`               | `format-on`                | When to apply the formatter. - 'blur': format on blur, show raw value on focus (default) - 'input': format on every keystroke (live formatting)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `"blur" \| "input"`                               | `'blur'`    |
| `formatter`              | --                         | Formats the raw value for display. Set via JS: `el.formatter = (v) => Number(v).toLocaleString()`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `((value: string) => string) \| undefined`        | `undefined` |
| `inputAriaAutocomplete`  | `input-aria-autocomplete`  | `aria-autocomplete` for the combobox — how typing relates to the popup.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | `"" \| "both" \| "inline" \| "list" \| "none"`    | `''`        |
| `inputExpanded`          | `input-expanded`           | Expanded state of the owner's popup. Emitted only with `input-role="combobox"`, where `aria-expanded` is an allowed attribute (it is not on a bare textbox).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | `boolean`                                         | `false`     |
| `inputMode`              | `inputmode`                |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `string`                                          | `''`        |
| `inputRole`              | `input-role`               | Promote the inner input to a WAI-ARIA **combobox**, for a composite that owns the popup (md-autocomplete). The role has to live on the real textbox — the one AT reads when it has focus — and this field renders it, so the owner cannot put it there itself.  Whitelisted to `combobox`: it is the only role an owner has a legitimate reason to place on a text input, and an open-ended role prop here would let a consumer lie about the control.                                                                                                                                                                                                                                                                                                                                                  | `"" \| "combobox"`                                | `''`        |
| `label`                  | `label`                    |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `string`                                          | `''`        |
| `max`                    | `max`                      |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `number \| string \| undefined`                   | `undefined` |
| `maxLength`              | `max-length`               |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `number \| undefined`                             | `undefined` |
| `min`                    | `min`                      |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `number \| string \| undefined`                   | `undefined` |
| `minLength`              | `min-length`               |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `number \| undefined`                             | `undefined` |
| `multiline`              | `multiline`                | Multi-line mode. - false: single-line <input> (default) - 'auto-grow': <textarea> that expands as content grows (multi-line field) - 'fixed': <textarea> with fixed height that scrolls (text area)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | `"auto-grow" \| "fixed" \| boolean`               | `false`     |
| `name`                   | `name`                     | Form field name (host attribute — ElementInternals submits under it).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | `string`                                          | `''`        |
| `parser`                 | --                         | Parses the display value back to a raw value. Set via JS: `el.parser = (v) => v.replace(/[^0-9.-]/g, '')`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | `((displayValue: string) => string) \| undefined` | `undefined` |
| `passwordToggle`         | `password-toggle`          | Show a password visibility toggle. 'internal' toggles the input type directly; 'external' emits mdPasswordToggle and leaves type control to the consumer.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | `"external" \| "internal" \| boolean`             | `false`     |
| `pattern`                | `pattern`                  | Constraint validation (forwarded to the inner input; surfaced via ElementInternals).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    | `string`                                          | `''`        |
| `placeholder`            | `placeholder`              |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `string`                                          | `''`        |
| `prefixText`             | `prefix-text`              |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `string`                                          | `''`        |
| `readOnly`               | `readonly`                 |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `boolean`                                         | `false`     |
| `required`               | `required`                 |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `boolean`                                         | `false`     |
| `reserveSupportingSpace` | `reserve-supporting-space` | Always occupy the supporting-text line, even when there is no message.  The row is otherwise rendered only when there is something to show, so the first validation error CREATES a line and shoves everything below it down — the whole form jumps as you tab through it. Reserving the space is opt-in because a field with no message would otherwise carry an empty line everywhere, including dense and inline layouts. createFormController turns it on automatically for the fields it manages, since those are exactly the ones whose messages appear and disappear.                                                                                                                                                                                                                            | `boolean`                                         | `false`     |
| `restrict`               | `restrict`                 | Restrict input to allowed characters. Presets: 'numeric', 'integer', 'decimal', 'alpha', 'alphanumeric'. Custom: a regex character class string, e.g. '[0-9+() -]'.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | `string`                                          | `''`        |
| `rows`                   | `rows`                     | Number of visible text rows for multiline modes. Defaults to 2 for auto-grow, 4 for fixed.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | `number \| undefined`                             | `undefined` |
| `speechLang`             | `speech-lang`              | Language/locale for internal speech recognition (e.g. 'en-US', 'de-DE'). Defaults to browser locale.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    | `string`                                          | `''`        |
| `speechToText`           | `speech-to-text`           | Speech-to-text via Web Speech API. - 'internal': manages recognition internally, appends transcript to value - 'external': emits mdSpeechResult on mic click, letting the consumer handle recognition                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | `"external" \| "internal" \| boolean`             | `false`     |
| `spellcheck`             | `spellcheck`               |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `boolean \| undefined`                            | `undefined` |
| `step`                   | `step`                     |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `number \| string \| undefined`                   | `undefined` |
| `suffixText`             | `suffix-text`              |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `string`                                          | `''`        |
| `supportingText`         | `supporting-text`          |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `string`                                          | `''`        |
| `throttle`               | `throttle`                 | Throttle interval (ms) for the mdSearch event. Emits at most once per interval while typing. 0 = disabled.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | `number`                                          | `0`         |
| `type`                   | `type`                     |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `string`                                          | `'text'`    |
| `value`                  | `value`                    | NOT reflected: typed values (incl. passwords) must never appear as a DOM attribute.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | `string`                                          | `''`        |
| `variant`                | `variant`                  |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `"filled" \| "outlined"`                          | `'filled'`  |


## Events

| Event              | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | Type                                                                                          |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------- |
| `mdChange`         |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | `CustomEvent<string>`                                                                         |
| `mdClear`          |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | `CustomEvent<void>`                                                                           |
| `mdInput`          |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | `CustomEvent<string>`                                                                         |
| `mdPasswordToggle` |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | `CustomEvent<{ visible: boolean; }>`                                                          |
| `mdSearch`         | Emitted with debounce/throttle control. Useful for search-as-you-type, API calls, or filtering.                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | `CustomEvent<string>`                                                                         |
| `mdSpeechResult`   |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | `CustomEvent<{ transcript: string; listening: boolean; }>`                                    |
| `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, delegated to the inner input.

#### Returns

Type: `Promise<boolean>`



### `getInputElement() => Promise<HTMLInputElement | HTMLTextAreaElement | null>`

The real `<input>` / `<textarea>` this field renders.

A composite that owns a popup (md-autocomplete, the date/time pickers)
needs the actual textbox to wire combobox ARIA onto — its role, its
expanded state, and the element references that point at a listbox living
in the PARENT's shadow root. Handing it over explicitly replaces the
shadow-piercing `querySelector('.md-text-field__input')` those components
were doing against a private class name.

Returns null before the first render.

#### Returns

Type: `Promise<HTMLInputElement | HTMLTextAreaElement | null>`



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

Native-parity validity readback: flags + message + willValidate.

#### Returns

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



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



#### Returns

Type: `Promise<boolean>`



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

Select the input's full contents.

#### Returns

Type: `Promise<void>`



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

App/server-side validation: a non-empty message marks the field
invalid for its form until cleared with an empty string.

#### Parameters

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

#### Returns

Type: `Promise<void>`



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

Focus the inner input (host.focus() also works via delegatesFocus).

#### Returns

Type: `Promise<void>`




## Shadow Parts

| Part                | Description |
| ------------------- | ----------- |
| `"chips"`           |             |
| `"container"`       |             |
| `"counter"`         |             |
| `"input"`           |             |
| `"label"`           |             |
| `"supporting-text"` |             |


## Dependencies

### Used by

 - [md-autocomplete](../md-autocomplete)
 - [md-date-picker](../md-date-picker)
 - [md-multi-select](../md-multi-select)
 - [md-number-field](../md-number-field)
 - [md-select](../md-select)
 - [md-time-picker](../md-time-picker)
 - [md-transfer-list](../md-transfer-list)

### Graph
```mermaid
graph TD;
  md-autocomplete --> md-text-field
  md-date-picker --> md-text-field
  md-multi-select --> md-text-field
  md-number-field --> md-text-field
  md-select --> md-text-field
  md-time-picker --> md-text-field
  md-transfer-list --> md-text-field
  style md-text-field fill:#f9f,stroke:#333,stroke-width:4px
```

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

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

AWC UI Operator’s Manual

# main-llm.md — AWC UI build director

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

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

Your job, in order:

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

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

---

## §1 — Interview the user

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

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

### 1.1 Scope and shape

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

### 1.2 Look and feel

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

### 1.3 Internationalization

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

### 1.4 Data and forms

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

### 1.5 Constraints

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

---

## §2 — Map answers to configuration

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

### 2.1 Global switches — the complete set

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

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

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

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

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

---

## §3 — Install and bootstrap

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

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

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

defineCustomElements(window);
```

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

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

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

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

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

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

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

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

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

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

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

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

---

## §4 — Global configuration reference

### 4.1 The token system

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

#### Color roles

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

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

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

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

#### Shape tokens

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

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

#### Elevation tokens

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

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

#### Motion tokens

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

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

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

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

#### State-layer opacities

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

#### Typography, spacing and layering

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

### 4.2 Theming and rebranding

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

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

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

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

### 4.3 Density

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

Two details that bite:

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

### 4.4 Dark mode

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

### 4.5 RTL

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

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

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

### 4.6 Internationalization

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

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

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

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

### 4.7 Forms and validation

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

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

Rules:

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

---

## §5 — Component decision matrix

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

### 5.1 Actions

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

### 5.2 Text input

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

### 5.3 Selection

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

### 5.4 Navigation

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

### 5.5 Containment and feedback

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

### 5.6 Data

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

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

### 5.7 Choosing the variant

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

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

### 5.8 Not in the library

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

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

---

## §6 — Component inventory

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

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

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

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

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

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

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

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

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

---

## §7 — Composition rules

### 7.1 What nests inside what

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

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

### 7.2 Pairs that belong together

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

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

### 7.3 Nesting that is always wrong

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

---

## §8 — Page recipes

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

### 8.1 Login screen

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

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

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

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

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

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

### 8.2 Settings page (mobile)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

### 8.4 Dashboard with a FAB

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

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

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

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

### 8.5 Destructive confirmation dialog

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

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

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

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

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

### 8.6 Tabbed content page

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

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

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

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

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

### 8.7 The full recipe library

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

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

---

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

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

### 9.1 API and styling

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

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

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

### 9.2 Content and hierarchy

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

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

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

### 9.3 Accessibility contract

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

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

---

## §10 — Before you ship

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