Skip to content

Search

A full search surface, not a text field — a bar that opens into a results panel. Two orthogonal axes (contained / divided × full-screen / docked) cover all four spec-canonical configurations, with debounced querying, a built-in voice affordance and an external trigger.

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-search placeholder="Search mail" style="max-inline-size: 480px;">
  <md-avatar slot="trailing" initials="MV" size="small"></md-avatar>
</md-search>

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


<md-search></md-search>
  • Search is a primary way in — mail, files, a catalogue, a large settings surface.
  • Results arrive while typing — suggestions update per keystroke, not after a page load.
  • The query is open-ended — free text, not a pick from a known set.
SituationUse instead
Filtering a list already on screenmd-text-field with a debounce
Picking one known valuemd-select / md-autocomplete
Search as a secondary action in the headermd-app-bar’s search variant
A handful of preset filtersmd-chip
VariantLookUse for
containedExpressive filled pillDefault. Search as a first-class surface
dividedBaseline bar with a hairline dividerSearch inside a denser app frame

Both variants are available in both layouts, so contained × full-screen, contained × docked, divided × full-screen and divided × docked are all spec-canonical and all four are fully supported.

Contained and divided — click either to open it
contained divided
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-search variant="contained" placeholder="Search mail"></md-search>
<md-search variant="divided" placeholder="Search mail"></md-search>
LayoutOpens asUse for
full-screenOpaque full-viewport surface, scroll-locked and focus-trappedDefault. Mobile, and search-first surfaces
dockedAnchored drawer under the bar, no scrim, no scroll lockDesktop, and search beside other content

While closed, layout="full-screen" (both contained and divided) shows a compact search icon button — not the resting search bar. Clicking it (or calling show()) opens the full-screen view. Override with trigger="bar" if you need the legacy resting bar entry point, or slot a custom control in slot="trigger".

When open is true, the results panel becomes an opaque surface that fills the entire viewport edge-to-edge (inset: 0, --md-sys-color-surface-container-low) and animates in. There is no scrim — the open/close transition matches md-dialog’s full-screen pattern: the surface and the bar rise together from translateY(30px) to 0 while fading in (no dimmer, no scale, no container-grow, no gray bands). The bar floats on top of the surface. Behaviour differs by variant:

VariantBar chrome
containedFloating expressive pill bar, inset from the viewport edges by --md-search-fullscreen-bar-offset-inline (8px) / --md-search-fullscreen-bar-offset-block-start (16px). Its gutters sit over the opaque results surface, so they read as the surface — never a page band
dividedEdge-bleed flat app-bar header (no rounding) flush to the viewport top (safe-area only)

Because the panel surface fills inset: 0 (behind the bar too) there are no gaps for the page to show through. Override the surface colour via --md-search-fullscreen-results-color.

The component:

  • Locks document scroll while open
  • Traps focus inside the bar + panel
  • Closes on Escape (configurable via escape-closes)
  • Closes via the leading back/close button
  • Renders a close button in place of the leading icon — chevron_left for the Expressive contained variant, arrow_back for the baseline divided variant. Override per-instance via open-leading-icon.

Tune contained full-screen spacing with --md-search-fullscreen-bar-offset-block-start, --md-search-fullscreen-bar-offset-inline, and --md-search-fullscreen-bar-panel-gap.

Full-screen — click either icon to take over the viewport, Escape to close
contained divided
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-search variant="contained" layout="full-screen" placeholder="Search mail">
  <md-list slot="results">
    <md-list-item headline="Quarterly report.pdf" supporting-text="Shared folder"></md-list-item>
    <md-list-item headline="Roadmap.key" supporting-text="Recent"></md-list-item>
  </md-list>
</md-search>

When layout="docked" and open is true, the results drawer anchors beneath the bar with just enough offset to clear the bar’s focus ring (8px by default — focus-ring offset + thickness + 3px), a content-driven height (no fixed empty bay — the drawer shrinks to fit few rows), and a max-height of min(400px, 60vh) (--md-search-panel-max-block-size). The drawer’s inline width matches the open bar exactly — both are inset by the focused expand margin (--md-search-expand-focused-inset, 12px), so the bar and the drawer share identical left and right edges across the whole responsive range (the overall component stays within the 360–720px clamp). The component:

  • Does not lock document scroll
  • Does not show a scrim
  • Closes on outside click (configurable via dismiss-on-outside-click)
  • Closes on Escape (configurable via escape-closes)
Docked — results drop under the bar, the page keeps scrolling
contained divided
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-search layout="docked" placeholder="Search mail">
  <md-list slot="results">
    <md-list-item headline="Quarterly report.pdf" supporting-text="Shared folder"></md-list-item>
    <md-list-item headline="Roadmap.key" supporting-text="Recent"></md-list-item>
  </md-list>
</md-search>

The MD3 spec lists the following anatomy items. They map to the API surface as follows:

Spec itemAPI
Search bar container[part="bar"]
Leading iconleading-icon prop, or slot="leading" for full replacement
Supporting text (placeholder hint)placeholder prop
Trailing icon and avatar (optional)slot="trailing" (any number / combination of icon-buttons + avatar)
Input textvalue prop / mdInput event / [part="input"]
Container for search suggestions or resultsslot="results", rendered inside [part="panel"][part="panel-body"][part="results-host"][part="results-viewport"]
Divider (divided variant)[part="divider"]
Search icon trigger (full-screen closed)[part="trigger"] / [part="trigger-button"] or slot="trigger"

The spec calls out four canonical trailing clusters. All are produced just by filling the trailing slot:

The four canonical trailing clusters — all just slot content
avatar one icon two icons icon + avatar
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-search>
  <md-icon-button slot="trailing" icon="mic" aria-label="Voice search"></md-icon-button>
  <md-avatar slot="trailing" name="Jane Doe"></md-avatar>
</md-search>

The component renders a built-in clear (×) button automatically as soon as the input has a value. Set show-clear-button="false" to opt out.

Set voice-search to render a built-in microphone button in the trailing cluster, wired to the browser’s Web Speech API (SpeechRecognition / webkitSpeechRecognition).

Clicking the mic starts recognition and streams the interim + final transcript straight into the input — firing the normal mdInput / mdSearch flow exactly as if the text had been typed — and the mic shows a pulsing “listening” state while active. Recognition stops automatically on the final result, on Escape, or when the panel closes. Listen to the mdVoice event for the raw transcript stream, style the button via ::part(voice-button), or drive it programmatically with the startVoice() / stopVoice() methods. For a fully custom mic, slot your own <md-icon-button slot="trailing"> instead.

Built-in voice search — the transcript streams straight into the input
Click the mic and speak. Where the Web Speech API is unavailable the mic is not rendered at all.
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-search layout="docked" voice-search placeholder="Search by voice"></md-search>

<script type="module">
  var el = document.querySelector('[data-voice]');
  var log = document.querySelector('[data-voice-log]');
  el.addEventListener('mdVoice', function (e) {
    log.textContent = (e.detail.final ? 'final: ' : 'interim: ') + JSON.stringify(e.detail.value);
  });
</script>

The built-in icon trigger and slot="trigger" both have to live inside md-search. When the opener belongs somewhere else on the page — an app-bar icon, an overflow menu item, a keyboard-shortcut hint — point trigger-for at it instead. The component wires that element rather than rendering it:

  • Activating it toggles the view, and activating it again while open closes it — the outside-click dismissal is exempted for the trigger, so a single click never closes and reopens in the same gesture.
  • aria-haspopup="dialog" is set on it, and aria-expanded tracks open.
  • Closing returns focus to it.
  • The search renders nothing at rest — no icon, no bar — so the page never offers two ways in.

Use a real button. Activation rides on click, which native buttons, md-button and md-icon-button already emit for Enter and Space.

<md-icon-button id="app-bar-search" icon="search" aria-label="Search files"></md-icon-button>
<md-search trigger-for="#app-bar-search"></md-search>
An app-bar icon button drives a search that renders nothing at rest
Files
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-icon-button id="app-bar-search" icon="search" aria-label="Search files"></md-icon-button>

<md-search trigger-for="#app-bar-search" layout="docked" placeholder="Search files">
  <md-list slot="results">
    <md-list-item headline="Quarterly report.pdf"></md-list-item>
    <md-list-item headline="Roadmap.key"></md-list-item>
  </md-list>
</md-search>
StateToken usage
EnabledContainer: --md-sys-color-surface-container-high
HoverState layer at --md-sys-state-hover-state-layer-opacity
FocusNo state-layer wash — the layer is zeroed and a --md-search-focus-indicator-color ring is drawn instead (3px at 2px offset, keyboard-only). The bar’s side inset springs from 24px → 12px (Expressive motion)
Pressedmd-ripple plus state layer at --md-search-pressed-state-layer-opacity (literal 0.1)
DisabledBar at 38% opacity, ripple disabled, input disabled
Enabled, populated, loading and disabled
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<div style="display:grid; gap:20px; max-inline-size:420px;">
  <md-search layout="docked" placeholder="Enabled"></md-search>
  <md-search layout="docked" placeholder="With a value" value="material design"></md-search>
  <md-search layout="docked" placeholder="Loading" value="design" loading></md-search>
  <md-search layout="docked" placeholder="Disabled" disabled></md-search>
</div>
EventDetailFires when…
mdInput{ value: string }The user types into the input or clears it via the × button (immediate, every keystroke)
mdSearch{ value: string }A query is ready to fetch — debounced/throttled + distinct-until-changed (trimmed). The event to wire your async fetch to.
mdSubmit{ value: string }The user presses Enter inside the input
mdChange{ value: string }Focus leaves the input and the value differs from focus-time
mdOpenvoidThe panel transitions from closed → open
mdClosevoidThe panel transitions from open → closed
mdClearvoidThe user activates the built-in clear (×) button
mdVoice{ value: string; final: boolean }Voice search is active and a transcript chunk streamed in (final true on the recognised result)
mdLeadingIconClickMdSearchLeadingIconClickDetailThe interactive leading affordance is clicked — the morphing back/dismiss button while open, or a custom slotted leading icon. Additive (the default back button still dismisses). The closed resting search glyph is not a click target, so it never emits.
mdTrailingIconClickMdSearchTrailingIconClickDetailA slotted trailing affordance is clicked. The built-in clear (×) and voice (mic) buttons emit mdClear / mdVoice instead and never trigger this. Only fires when slotted trailing content is present and was the click target.
Every event, live — mdInput fires per keystroke, mdSearch after the debounce
Type, press Enter, then clear — oldest first. Watch mdSearch lag mdInput by the 300ms debounce.
Show code for each technology
<md-search id="docs-search" layout="docked" debounce="300" placeholder="Search the docs…"></md-search>

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

search.addEventListener('mdInput', (e) => {
  const { value } = e.detail;                 // every keystroke
  console.log('typing', value);
});

search.addEventListener('mdSearch', (e) => {
  const { value } = e.detail;                 // debounced + de-duplicated
  console.log('fetch', value);
});

search.addEventListener('mdSubmit', (e) => {
  console.log('submit', e.detail.value);      // Enter inside the input
});

search.addEventListener('mdClear', () => {
  console.log('cleared');
});
</script>

mdInput is a keystroke: it fires on every character, including the ones the user is about to delete. mdSearch is a query: it is debounced, throttled and de-duplicated on the trimmed value, so wiring a fetch to it costs one request per intention rather than one per key.

Wire an asynchronous fetch to the mdSearch event rather than mdInput. mdSearch is rate-limited so you don’t hammer the server on every keystroke:

  • debounce (ms) — waits for a typing pause before emitting. 0 (default) emits on every change.
  • throttle (ms) — a maxWait safety net: during sustained typing where the debounce never settles, force an emit at least this often so the user still sees interim results. 0 (default) disables it; only meaningful alongside a non-zero debounce.
  • distinct-until-changed — the emit is de-duplicated on the trimmed query, so re-typing the same term (or only adding surrounding whitespace) won’t re-trigger a fetch. Pressing Enter or clearing the field flushes immediately.

Set loading while the request is in flight to show the M3 loading indicator (the looping shape-morph) in the bar’s trailing cluster; it replaces the clear button until the fetch resolves and is announced to assistive tech via the live region (loading-label, default “Searching”). The swap is an expressive cross-fade — the loading indicator and clear-× morph (opacity + scale + rotate) into one another in both directions over the same fixed-size box, so the trailing cluster never jumps.

Debounced fetch with a loading indicator and out-of-order guard
Type “a” — the fetch is faked with a 700ms delay so the loading indicator is visible.
Show code for each technology
<md-search id="product-search" layout="docked" debounce="300" throttle="1000" placeholder="Search products">
<md-list slot="results" id="product-results"></md-list>
</md-search>

<script type="module">
const search = document.getElementById('product-search');
let token = 0;

search.addEventListener('mdSearch', async (e) => {
  const { value } = e.detail;
  const ticket = ++token;                 // ignore out-of-order responses

  if (!value) {
    search.loading = false;
    renderResults([]);
    return;
  }

  search.loading = true;
  try {
    const results = await fetchFromServer(value);
    if (ticket !== token) return;         // a newer query superseded this one
    renderResults(results);
  } finally {
    if (ticket === token) search.loading = false;
  }
});
</script>

Properties

PropertyAttributeTypeDefaultReflects
variantvariant'contained' | 'divided''contained'Yes
layoutlayout'full-screen' | 'docked''full-screen'Yes
triggertrigger'bar' | 'icon'Yes
triggerIcontrigger-iconstring'search'
triggerFortrigger-forstring''
triggerElementJS onlyHTMLElement
fullWidthfull-widthbooleanfalseYes
valuevaluestring''Yes
placeholderplaceholderstring'Search'
openopenbooleanfalseYes
disableddisabledbooleanfalseYes
elevationelevation0 | 1 | 2 | 3 | 4 | 50Yes
leadingIconleading-iconstring'search'
openLeadingIconopen-leading-iconstring
showClearButtonshow-clear-buttonbooleantrue
voiceSearchvoice-searchbooleanfalse
escapeClosesescape-closesbooleantrue
dismissOnOutsideClickdismiss-on-outside-clickbooleantrue
scrollShadowscroll-shadowbooleantrueYes
maxBlockSizemax-block-sizestring''
inputAriaLabelinput-aria-labelstring''
initialFocusinitial-focus'auto' | 'input' | 'leading''auto'
announceResultsannounce-resultsbooleantrue
resultsLabelresults-labelstring'{count} results available'
noResultsLabelno-results-labelstring'No results available'
loadingloadingbooleanfalseYes
debouncedebouncenumber0
throttlethrottlenumber0
loadingLabelloading-labelstring'Searching'
densitydensity0 | -1 | -2 | -3 | -40Yes

Methods

MethodParameters
show()none
close()none
toggle()none
focusInput()none
startVoice()none
stopVoice()none

Slots

SlotDescription
triggerReplace the built-in search icon trigger (full-screen)
leadingReplace the leading icon
loaderReplace the in-bar loading indicator shown while `loading`
trailingTrailing icon buttons + avatar (any combination)
resultsSuggestions / results list

CSS Custom Properties

Override on the host element for per-instance theming:

PropertyDescription
--md-search-container-colorBar background (#ECE6F0 / surface-container-high)
--md-search-container-elevationCustom bar shadow — overrides the
--md-search-leading-icon-colorLeading / open-back icon (#1D1B20 / on-surface)
--md-search-trailing-icon-colorTrailing + clear icons (#49454F / on-surface-variant)
--md-search-icon-colorAlias for trailing icon colour (legacy)
--md-search-loading-colorIn-bar loading indicator shape colour
--md-search-container-shapeBar border-radius
--md-search-container-heightBar block-size — docked / contained (56px)
--md-search-icon-button-sizeLeading / trailing icon tap target (48px)
--md-search-leading-icon-label-gapTap target → label gap (4px)
--md-search-trailing-icon-label-gapLabel → tap target gap (4px)
--md-search-container-padding-inlineExtra inline padding on the bar (0px)
--md-search-container-padding-inline-focusedInline padding on the bar when focused / open
--md-search-leading-padding-inlineSpace from the bar's leading edge
--md-search-trailing-padding-inlineSpace from the bar's trailing edge
--md-search-input-colorInput text colour
--md-search-input-font-familyInput typescale font (body-large)
--md-search-input-font-sizeInput typescale size (16px)
--md-search-input-line-heightInput typescale line height (24px)
--md-search-input-font-weightInput typescale weight (400)
--md-search-input-letter-spacingInput typescale tracking (0.5px)
--md-search-placeholder-colorPlaceholder / supporting text colour
--md-search-state-layer-colorHover/press wash (#1D1B20 / on-surface)
--md-search-focus-indicator-colorFocus ring colour (#625B71 / secondary)
--md-search-focus-indicator-offsetFocus ring offset (2px)
--md-search-avatar-sizeAvatar diameter when slotted (40px)
--md-search-panel-min-block-sizeDocked panel floor — 0 = shrink to
--md-search-panel-max-block-sizeDocked panel maximum height —
--md-search-max-block-sizeOpen surface max height — full-screen
--md-search-docked-panel-shapeContained docked results card radius
--md-search-panel-offsetDocked panel gap fallback (divided / generic 4px)
--md-search-bar-fullscreen-colorBar background in full-screen open
--md-search-fullscreen-results-colorOpaque results surface in full-screen
--md-search-divider-colorHairline colour (divided variant)
--md-search-divider-thicknessHairline thickness (1px)
--md-search-fullscreen-bar-panel-gapContained full-screen: gap between
--md-search-empty-colorNo-results message colour
--md-search-empty-padding-blockNo-results vertical padding
--md-search-empty-padding-inlineNo-results horizontal padding
--md-search-expand-insetDocked/inline bar RESTING side
--md-search-expand-focused-insetDocked/inline bar FOCUSED side
--md-search-expand-durationSpringy expand duration (medium4 / 400ms)
--md-search-expand-easingSpringy expand easing — back-out
--md-search-panel-motion-durationDocked panel + bar transition
--md-search-fullscreen-expand-durationFull-screen modal enter duration
--md-search-fullscreen-expand-easingFull-screen enter easing
--md-search-fullscreen-collapse-easingFull-screen exit easing
--md-search-divided-fullscreen-bar-height
--md-search-container-min-inline-size
--md-search-container-max-inline-size
--md-search-icon-size
--md-search-focus-indicator-thickness
--md-search-panel-padding-block-start

CSS Shadow Parts

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

PartDescription
trailingTrailing slot container (icon buttons / avatar)
triggerIcon-button entry point (full-screen closed state)
trigger-buttonBuilt-in search icon button (when trigger slot empty)
leadingLeading icon container
leading-state-layer
clear-buttonBuilt-in clear (×) button
voice-buttonBuilt-in voice-search mic button (when `voice-search`
loadingIn-bar loading indicator (shown while `loading`)
statusVisually-hidden polite live region (result announcements)
barSearch bar container
state-layerHover / focus / press overlay on the bar
inputNative <input>
panelResults panel — full-viewport surface (full-screen) /
dividerHairline between bar and panel (visible in `divided`)
panel-bodyFlex region that sizes the scroll viewport
emptyVisible no-results message (open + query + zero rows)
results-host
results-viewportInner scroll viewport (md-scroll-shadow)
loading-shapeMorphing shape inside the loading indicator
  • Role & labeling. The bar’s <input> has role="combobox" with aria-autocomplete="list" (APG popup search / autosuggest pattern). Per the M3 labeling guidance the hinted search text describes the bar, so the accessible name defaults to placeholder; set input-aria-label only when the visible hint and the accessible name should differ. aria-expanded mirrors open and aria-controls points to the results panel.
  • Result announcements. Render suggestions with md-list + md-list-item so screen readers announce them as a list. A visually-hidden polite live region (role="status", part="status") announces the count whenever the slotted list changes while the panel is open. Customize with results-label ({count} token) and no-results-label, or disable it with announce-results="false".
  • Visible empty state. Open, with a query and zero results, a visible message (part="empty") shows the same no-results-label copy (default "No results available"); the hidden live region defers to it so the string is never announced twice.
  • Initial focus. The resting bar’s first tab stop is a slotted leading icon button when one is present, otherwise the text field — a non-interactive resting glyph stays out of the tab order. On open, focus moves to the text field (initial-focus="auto" / "input"); use initial-focus="leading" to focus the leading button instead. Focus is restored on close.
  • Modality. The panel has role="dialog". aria-modal is "true" only when layout is full-screen and open is true; docked panels are popup-style and stay non-modal so the rest of the page stays traversable.
  • Naming the affordances. The open full-screen leading button carries aria-label="Close search" and the clear (×) button "Clear search". Label any slotted leading / trailing icon buttons yourself.
  • Keyboard. The full set of keys the component handles:
KeysAction
Tab / Shift+TabNavigate between interactive elements. In the full-screen layout focus is trapped to the bar + panel + slotted buttons and cycles without leaving the dialog.
Space / EnterActivate the text field for input. While closed they open the search view (focus alone never opens it); while open, Enter submits the query via mdSubmit.
Arrow Down / Arrow UpNavigate between slotted result items. Arrow Down from the field steps into the first item; Arrow Up from the first item returns to the field. Result rows are given a roving tabindex so focus can land on them.
EscapeClose the panel (configurable via escape-closes).

Tab into the bar, type a letter, then use Arrow Down to walk into the results and Escape to close. A screen reader announces the row count from the hidden live region as the list changes.

Keyboard and screen-reader behaviour
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-search layout="docked" placeholder="Search contacts" input-aria-label="Search contacts" results-label="{count} contacts available" style="max-inline-size:460px;">
  <md-icon-button slot="trailing" icon="tune" aria-label="Filters"></md-icon-button>
  <md-list slot="results">
    <md-list-item headline="Amelia Novak" supporting-text="amelia@example.com"></md-list-item>
    <md-list-item headline="Bruno Sadiq" supporting-text="bruno@example.com"></md-list-item>
    <md-list-item headline="Carmen Diaz" supporting-text="carmen@example.com"></md-list-item>
  </md-list>
</md-search>

The trailing tune button is yours to label — the built-in clear (×) and mic already carry their own names. Verified against axe-core with zero WCAG 2.1 AA violations.

RTL — every box metric is a logical property (inset-inline, padding-inline, margin-inline-start…), so the bar mirrors automatically inside any dir="rtl" ancestor. The leading and trailing slots swap visually with no consumer-side adjustment. See RTL.

Same markup, dir=ltr vs dir=rtl — leading and trailing swap
ltr
rtl
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;">ltr</span>
  <div dir="ltr"><md-search layout="docked" placeholder="Search mail" style="max-inline-size: 440px;"><md-avatar slot="trailing" initials="AL" size="small"></md-avatar></md-search></div>
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">rtl</span>
  <div dir="rtl"><md-search layout="docked" placeholder="بحث في البريد" style="max-inline-size: 440px;"><md-avatar slot="trailing" initials="AL" size="small"></md-avatar></md-search></div>
</div>

md-search takes a local density rung (-1-4) and also honours a global data-density ancestor; a local -1-4 rung wins over the inherited one. 0 is the undensified baseline you get when nothing sets a rung. Each rung takes 4px off the bar (56px at 0, floor 40px), 4px off the leading / trailing tap targets (48px at 0, floor 32px), 1px off the icon glyphs (24px at 0, floor 18px) and 0.5px off the input type (16px at 0, floor 13px). Override any of it directly with --md-search-container-height or --md-search-icon-button-size.

Density 0 through -4 — bar height, tap targets, glyphs and input type 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>
  <md-search layout="docked" density="0" placeholder="Search mail" style="max-inline-size:420px;"></md-search>
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">-1</span>
  <md-search layout="docked" density="-1" placeholder="Search mail" style="max-inline-size:420px;"></md-search>
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">-2</span>
  <md-search layout="docked" density="-2" placeholder="Search mail" style="max-inline-size:420px;"></md-search>
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">-3</span>
  <md-search layout="docked" density="-3" placeholder="Search mail" style="max-inline-size:420px;"></md-search>
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">-4</span>
  <md-search layout="docked" density="-4" placeholder="Search mail" style="max-inline-size:420px;"></md-search>
</div>
dir=rtl with data-density=-2 — the second bar tightens further with density=-4, the third resets to the baseline with --md-sys-density-scale: 0
Show code for each technology
<!-- 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;flex-direction:column;gap:16px;inline-size:100%;max-inline-size:460px;">
  <md-search layout="docked" placeholder="بحث في البريد"><md-avatar slot="trailing" initials="AL" size="small"></md-avatar></md-search>
  <md-search layout="docked" density="-4" placeholder="بحث في الملفات"><md-avatar slot="trailing" initials="AL" size="small"></md-avatar></md-search>
  <md-search layout="docked" style="--md-sys-density-scale: 0;" placeholder="بحث في الصور"><md-avatar slot="trailing" initials="AL" size="small"></md-avatar></md-search>
</div>
Localized placeholder, announcement strings and affordance names
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-search
  placeholder="Rechercher dans les messages"
  input-aria-label="Rechercher"
  results-label="{count} résultats disponibles"
  no-results-label="Aucun résultat"
  loading-label="Recherche en cours"
  >
  <md-icon-button slot="trailing" icon="mic" aria-label="Recherche vocale"></md-icon-button>
</md-search>

Density — the rungs are declared on :host, so a local density of -1-4 out-ranks the inherited data-density ancestor value. There is no [density="0"] rule, so 0 inherits instead of resetting; use --md-sys-density-scale: 0 for that. See Density.

i18n — translate placeholder, input-aria-label, results-label, no-results-label and loading-label, plus every slotted control’s aria-label. The results you slot are yours to localize.

Custom propertyPurposeDefault
--md-search-container-colorBar background--md-sys-color-surface-container-high
--md-search-container-elevationCustom bar shadow — overrides the elevation prop level0px 0px 0px 0px transparent (a transparent zero-shadow, not none — see the caution below)
--md-search-container-shapeBar border-radius--md-sys-shape-corner-full
--md-search-container-heightBar block-size (contained + divided docked)56px at density 0
--md-search-divided-fullscreen-bar-heightDivided × full-screen header height72px at density 0
--md-search-container-min-inline-sizeBar minimum width360px
--md-search-container-max-inline-sizeBar maximum width720px
--md-search-container-padding-inlineExtra inline padding on the bar (rare; defaults to 0 so the clusters control their own gutters)0px
--md-search-container-padding-inline-focusedInline padding when focused / openinherits resting
--md-search-leading-padding-inlineBar leading edge → leading tap target4px
--md-search-trailing-padding-inlineBar trailing edge → trailing tap target8px
--md-search-icon-button-sizeLeading / trailing tap-target diameter (M3 48px)48px at density 0
--md-search-leading-icon-label-gapLeading tap target → input text4px
--md-search-trailing-icon-label-gapInput text → trailing tap target4px
--md-search-input-colorInput text colour--md-sys-color-on-surface
--md-search-input-font-familyInput / placeholder font (M3 body-large)--md-sys-typescale-body-large-font-family
--md-search-input-font-sizeInput / placeholder size16px at density 0
--md-search-input-line-heightInput / placeholder line height24px
--md-search-input-font-weightInput / placeholder weight400
--md-search-input-letter-spacingInput / placeholder tracking0.5px
--md-search-placeholder-colorPlaceholder (supporting text) colour--md-sys-color-on-surface-variant
--md-search-leading-icon-colorLeading / back icon colour--md-sys-color-on-surface
--md-search-trailing-icon-colorTrailing + clear icon colour--md-sys-color-on-surface-variant
--md-search-icon-colorAlias for trailing icon coloursame as trailing
--md-search-icon-sizeIcon font-size24px at density 0
--md-search-loading-colorIn-bar loading indicator shape colour--md-sys-color-primary
--md-search-state-layer-colorHover / press wash--md-sys-color-on-surface
--md-search-focus-indicator-colorFocus ring colour--md-sys-color-secondary
--md-search-focus-indicator-thicknessFocus ring thickness3px
--md-search-focus-indicator-offsetFocus ring offset2px
--md-search-avatar-sizeSlotted avatar diameter40px
--md-search-panel-min-block-sizeDocked panel floor — 0 shrinks to content0px
--md-search-panel-max-block-sizeDocked panel maximum height; results beyond it scrollmin(400px, 60vh)
--md-search-max-block-sizeOpen surface cap — full-screen overlay or docked panel (overrides panel max when set)unset
--md-search-docked-panel-shapeContained docked results card corner radius16px
--md-search-panel-offsetGap between bar and docked panelfocus-ring offset + thickness + 3px (8px)
--md-search-contained-docked-panel-offsetContained docked: bar ↔ results gapfalls back to --md-search-panel-offset’s formula (8px)
--md-search-empty-colorNo-results empty-state text colour--md-sys-color-on-surface-variant
--md-search-empty-padding-blockNo-results empty-state vertical padding24px
--md-search-empty-padding-inlineNo-results empty-state horizontal padding16px
--md-search-bar-fullscreen-colorDivided full-screen bar background while open--md-sys-color-surface-container-low
--md-search-fullscreen-results-colorOpaque full-viewport results surface (no scrim)--md-sys-color-surface-container-low
--md-search-fullscreen-bar-offset-block-startFull-screen inset from viewport topmax(16px, safe-area)
--md-search-fullscreen-bar-offset-inlineContained full-screen bar inset from inline edgesmax(8px, safe-area)
--md-search-fullscreen-bar-panel-gapContained full-screen gap between bar and panelfocus-ring offset + thickness + 3px (8px) for contained; 0px for divided, which wants a flush seam under its hairline
--md-search-panel-padding-block-startFull-screen panel padding above results0px
--md-search-fullscreen-expand-durationFull-screen open (slide + fade) duration--md-sys-motion-duration-medium2 (300ms fallback)
--md-search-fullscreen-expand-easingFull-screen open easing--md-sys-motion-easing-standard
--md-search-fullscreen-collapse-durationFull-screen close duration — keep it in sync with the component’s 300ms close timer--md-sys-motion-duration-medium2 (300ms fallback)
--md-search-fullscreen-collapse-easingFull-screen close easing--md-sys-motion-easing-standard
--md-search-divider-colorHairline colour (divided variant)--md-sys-color-outline
--md-search-divider-thicknessHairline thickness1px
--md-search-expand-insetDocked/inline bar resting side margin24px
--md-search-expand-focused-insetDocked/inline bar focused side margin12px
--md-search-expand-durationSpringy expand duration (also drives the synced ripple wave)medium4 (400ms)
--md-search-expand-easingSpringy back-out curve — overshoots ~17% then snaps backcubic-bezier(0.34, 1.56, 0.64, 1)
Themed instances — shape, height, tap-target size and a branded bar
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<div style="display:flex;flex-direction:column;gap:20px;inline-size:100%;">
  <md-search layout="docked" placeholder="Default" style="max-inline-size: 440px;"></md-search>
  <md-search layout="docked" placeholder="Squared and short" style="max-inline-size: 440px; --md-search-container-shape: 8px; --md-search-container-height: 44px; --md-search-icon-button-size: 40px;"></md-search>
  <md-search layout="docked" placeholder="Branded" style="max-inline-size: 440px; --md-search-container-color: var(--md-sys-color-primary-container); --md-search-input-color: var(--md-sys-color-on-primary-container);"></md-search>
</div>

Every default resolves through an md-sys-color role, so a bar 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-search layout="docked" placeholder="Untouched defaults" style="max-inline-size: 460px;">
  <md-avatar slot="trailing" initials="AL" size="small"></md-avatar>
</md-search>

This is the single, unified expansion mechanism for all docked/inline barscontained and divided alike. The bar rests with a 24px side margin (--md-search-expand-inset) and springs to a 12px side margin (--md-search-expand-focused-inset) on focus / open, symmetrically (even left + right). Each edge travels the 24 − 12 = 12px delta, so the container grows evenly on both sides.

The default easing is a back-out spring curve: the bar briefly overshoots the target width by ~17 % and snaps back into place. Combined with a 400 ms duration this reads as deliberate, expressive, and a touch playful — the M3 expressive aesthetic — without needing the Web Animations API. The same duration drives the synced ripple wave so the press response and the bar settle land at the same instant.

Expansion — focus each bar and watch the side margins spring Open in Storybook
default theatrical no expand
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:4.5rem;opacity:.7;font-size:.8rem;font-family:ui-monospace,monospace;">default</span>
  <md-search layout="docked" placeholder="24 → 12px" style="max-inline-size:440px;"></md-search>
  <span style="inline-size:4.5rem;opacity:.7;font-size:.8rem;font-family:ui-monospace,monospace;">theatrical</span>
  <md-search layout="docked" placeholder="48 → 12px" style="max-inline-size:440px; --md-search-expand-inset: 48px; --md-search-expand-focused-inset: 12px; --md-search-expand-duration: 350ms;"></md-search>
  <span style="inline-size:4.5rem;opacity:.7;font-size:.8rem;font-family:ui-monospace,monospace;">no expand</span>
  <md-search layout="docked" placeholder="Fixed margin" style="max-inline-size:440px; --md-search-expand-inset: 12px; --md-search-expand-focused-inset: 12px;"></md-search>
</div>
/* Calmer, no overshoot — falls back to the standard M3 emphasized curve. */
md-search.calm {
--md-search-expand-easing: var(--md-sys-motion-easing-emphasized);
--md-search-expand-duration: 400ms;
}

Set the full-width boolean prop to make a docked/inline bar span its container edge-to-edge with no side gutters. It zeroes both expand insets for that instance (resting + focused = 0), so there is no 24 → 12px margin animation and the aligned results drawer fills the same full width. It has no effect on full-screen (already edge-to-edge).

full-width — zeroes both expand insets, so the bar and drawer bleed to the container edges
default full-width
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-search layout="docked" full-width placeholder="Search"></md-search>

full-width is purely a shorthand over the CSS vars — the identical effect is available by zeroing both insets directly:

md-search.bleed {
--md-search-expand-inset: 0;
--md-search-expand-focused-inset: 0;
}

Long lists scroll inside an inner viewport (part="results-viewport") so the scrollbar stays aligned with the slotted surface. Cap the scroll area with --md-search-panel-max-block-size (docked) or --md-search-max-block-size (full-screen overlay, or a docked override). The bar height stays on --md-search-container-height.

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

<md-search layout="docked" open value="Eli" placeholder="Search contacts" style="max-inline-size:440px; --md-search-panel-max-block-size: 220px;">
  <md-list slot="results">
    <md-list-item headline="Eli Bassi"></md-list-item>
    <md-list-item headline="Elias Fournier"></md-list-item>
    <md-list-item headline="Elif Demir"></md-list-item>
    <md-list-item headline="Elijah Mensah"></md-list-item>
    <md-list-item headline="Elisa Moretti"></md-list-item>
    <md-list-item headline="Ellis Ward"></md-list-item>
  </md-list>
</md-search>

CSS Partsbar, state-layer, leading, leading-state-layer, input, trailing, clear-button, voice-button, loading, loading-shape, trigger, trigger-button, panel, panel-body, divider, empty, results-host, results-viewport, status.

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>
  md-search.branded::part(bar) { border: 2px solid var(--md-sys-color-primary); }
  md-search.branded::part(input) { font-weight: 600; }
  md-search.branded::part(clear-button) { color: var(--md-sys-color-error); }
</style>
<md-search class="branded" layout="docked" value="invoice" placeholder="Search files" style="max-inline-size:440px; --md-search-container-color: var(--md-sys-color-surface-container-low);"></md-search>
md-search::part(bar) {
border: 2px solid var(--md-sys-color-primary);
}

md-app-bar · md-autocomplete · md-text-field · md-select · md-list · md-chip · md-icon-button

For AI Agents — md-search

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

<!-- llm:meta
tag: md-search
category: text-input
status: md3-mapped
m3-guidelines: https://m3.material.io/components/search/guidelines
form-associated: false
depends-on: md-icon-button, md-loading-indicator, md-ripple
used-by: none
-->

**A search entry point with its own results surface.** A bar (or icon trigger)
that expands into a full-screen or docked panel, with slotted suggestions and
results, optional voice input, and live-region announcements. It renders the
surface and reports the query — it never fetches or filters anything itself.

> ⚠️ **Not form-associated.** It is a search surface, not a form field — read
> `mdSearch` / `mdSubmit` and act on the query yourself.

> 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

- Finding things across a **large collection** — messages, files, products.
- Search that needs a **results surface** (suggestions as you type, recent
  searches, grouped results).
- The M3 "search bar" entry point: search scoped to the current view.

## When NOT to use

| Situation | Use instead |
|---|---|
| Choosing from a known list | `md-select` / `md-autocomplete` |
| Filtering an already-visible list in place | `md-text-field type="search"` + your own filter |
| Picking one value with type-ahead | `md-autocomplete` |
| Search as the app's **global primary** function | An app bar search variant — `md-app-bar` |
| A plain text input | `md-text-field` |

## Decision cues

| Need | Setting |
|---|---|
| Mobile / immersive | `layout="full-screen"` |
| Desktop dropdown panel | `layout="docked"` |
| Always-visible bar | `trigger="bar"` |
| Collapsed icon that expands | `trigger="icon"` (+ `trigger-icon`) |
| Opener lives elsewhere on the page | `trigger-for="#app-bar-search"` |
| Opener is an element you already hold | `el.triggerElement = ref` |
| Bar with a hairline rule above the results | `variant="divided"` |
| Standard filled bar | `variant="contained"` (default) |
| Bar should fill its container edge to edge | `full-width` |
| Dictation | `voice-search` |
| Throttle remote queries | `debounce="300"` (+ `throttle` as a max-wait) |
| Cap the panel height | `max-block-size` |
| Compact bar | `density="-1"` … `density="-4"` |

## API contract

```html
<md-search
  variant="contained|divided"        <!-- default: contained -->
  layout="full-screen|docked"        <!-- default: full-screen -->
  trigger="bar|icon"                 <!-- default: icon for full-screen, bar for docked -->
  trigger-icon="search"              <!-- default: search -->
  trigger-for="#app-bar-search"      <!-- document selector for an external opener -->
  value=""
  placeholder="Search"               <!-- default: Search -->
  open
  disabled
  full-width
  elevation="0"                      <!-- default: 0; 1-5 map to MD3 elevation tiers -->
  leading-icon="search"              <!-- default: search -->
  open-leading-icon="arrow_back"     <!-- default: chevron_left (contained) / arrow_back (divided) -->
  show-clear-button                  <!-- default: true -->
  voice-search                       <!-- default: false -->
  escape-closes                      <!-- default: true -->
  dismiss-on-outside-click           <!-- default: true -->
  max-block-size="60vh"
  input-aria-label="Search messages" <!-- default: falls back to placeholder -->
  initial-focus="auto|input|leading" <!-- default: auto -->
  announce-results                   <!-- default: true -->
  results-label="{count} results available"
  no-results-label="No results available"
  loading                            <!-- you set this; the component never flips it -->
  loading-label="Searching"
  debounce="0" throttle="0"          <!-- default: 0 (both off) -->
  density="-1|-2|-3|-4"              <!-- default: 0 (uncompacted) -->
>
  <div slot="results">…suggestions or results…</div>
</md-search>
```

`triggerElement` is a **property**, not an attribute — assign an element
reference in JS. `scroll-shadow` still parses but is a documented no-op.

**Events**

| Event | Detail | Fires |
|---|---|---|
| `mdInput` | `{ value }` | Immediately, on every keystroke — never debounced |
| `mdSearch` | `{ value }` | The query to run: gated by `debounce` / `throttle`, de-duplicated on the **trimmed** query, and `value` is that trimmed string |
| `mdSubmit` | `{ value }` | Enter pressed while the panel is open (a pending `mdSearch` is flushed first) |
| `mdChange` | `{ value }` | Blur, only when the value differs from what it was at focus |
| `mdOpen` / `mdClose` | `void` | Panel opened / closed |
| `mdClear` | `void` | Built-in clear (×) button pressed |
| `mdVoice` | `{ value, final }` | Voice transcript, interim (`final: false`) and final |
| `mdLeadingIconClick` | `{ value, open }` | Leading affordance activated — the open-state back button, or a slotted `leading` element |
| `mdTrailingIconClick` | `{ value, open }` | A **slotted** `trailing` element was clicked (the built-in clear and mic emit `mdClear` / `mdVoice` instead) |

**Methods** — `show()`, `close()`, `toggle()`, `focusInput()`, `startVoice()`,
`stopVoice()`.

**Slots** — `results` (the panel body — **you render this**), `leading`,
`trailing`, `trigger` (replaces the built-in trigger), `loader` (replaces the
built-in loading indicator in the trailing cluster while `loading`).

**Parts** — `trigger`, `trigger-button`, `bar`, `state-layer`, `leading`,
`leading-state-layer`, `input`, `clear-button`, `voice-button`, `trailing`,
`loading`, `status`, `panel`, `divider`, `panel-body`, `empty`,
`results-host`, `results-viewport`.

### Behavioral contract worth knowing

- **The component does not search.** It gives you the surface, the query and
  the panel; you render results into the `results` slot and run the query.
- **Wire exactly one query source.** `mdInput` fires on every keystroke,
  `mdSearch` is the debounced/de-duplicated one, `mdSubmit` fires on Enter and
  `mdChange` on a changed blur. Use `mdSearch` for the fetch; the other three
  will re-run the same query if you also bind them to it.
- `debounce` and `throttle` gate **`mdSearch` only**. `throttle` is a max-wait
  safety net that forces an emit during sustained typing, and is only
  meaningful alongside a non-zero `debounce`.
- **`mdSearch` carries the trimmed query** and is distinct-until-changed on it,
  so re-typing the same term (or adding surrounding whitespace) does not
  re-fire. Enter and the clear button **flush** it immediately past any pending
  debounce — clearing therefore delivers an empty query at once.
- **Focus does not open the panel.** Tabbing into the bar only moves focus;
  the view opens on typing, on Enter/Space, or on a click on the bar or
  trigger. The focus ring is keyboard-modality only.
- **`trigger` defaults by layout**: `icon` for `full-screen`, `bar` for
  `docked`. Set it explicitly to override.
- **The built-in trigger and the `trigger` slot both live inside the element.**
  When the opener belongs elsewhere — an app-bar icon, a menu item — use
  `trigger-for` (a document selector) or the `triggerElement` property
  (which wins over `trigger-for`). The component wires that element instead of
  rendering it: activation opens the view, `aria-haspopup="dialog"` and
  `aria-expanded` are kept in step, and focus returns to it on close. With
  either set the component renders no resting affordance of its own.
- **`full-screen` locks document scroll** while open (compensating for the
  scrollbar width so the page does not shift) and **traps Tab** inside the
  overlay. `docked` is non-modal: Tab continues into the document.
- While open, **ArrowDown / ArrowUp move focus through the slotted results**;
  Down from the input lands on the first item, Up from the first item returns
  to the input. The steppable rows are the ones matching `md-list-item`,
  `[role="option"]`, `[role="menuitem"]`, `[data-search-result]` or `li`.
- `escape-closes` and `dismiss-on-outside-click` both default **on**. The
  outside-click watcher runs in both layouts on `mousedown` (capture) and
  deliberately ignores clicks on a wired external trigger, so that trigger owns
  its own toggle instead of closing and reopening in one gesture.
- `open-leading-icon` swaps the leading glyph while the panel is open. Its
  default is variant-aware — `chevron_left` for `contained`, `arrow_back` for
  `divided`. The **closed** resting search glyph is not a click target (a click
  there focuses the input), so it emits no `mdLeadingIconClick`.
- **`loading` is yours to control.** The component never sets it; set it true
  before awaiting your fetch and false once results are slotted. It only drives
  the trailing indicator and the live-region announcement.
- `announce-results` + `results-label` drive a polite live region; `{count}`
  is replaced with the number of detected items — elements in the `results`
  slot (or descendants of them) matching `md-list-item`, `[role="option"]`,
  `li` or `[data-search-result]`, falling back to the count of slotted
  elements. `no-results-label` covers the empty case, and the live region
  defers to the visible empty state to avoid duplicate speech.
- **`data-search-result` is the opt-in hook for custom rows.** If your result
  rows are none of those element types (a `<div>` card, say), put
  `data-search-result` on each one so it is counted *and* reachable with the
  arrow keys.
- **Voice search is doubly gated**: `voice-search` must be true **and** the
  browser must expose `SpeechRecognition` / `webkitSpeechRecognition`. When the
  API is missing the mic button is simply not rendered — there is no dead
  button to guard against. Closing the panel stops any active session.
- `elevation` (`0`–`5`) maps to the MD3 elevation tiers; setting
  `--md-search-container-elevation` overrides the prop entirely.
- `full-width` is exactly equivalent to setting `--md-search-expand-inset: 0`
  and `--md-search-expand-focused-inset: 0`, and has no effect on
  `full-screen` (already edge-to-edge).
- `scroll-shadow` is **retained for compatibility and does nothing** — the
  results viewport always plain-scrolls.

---

## Do / Don't

Sourced from [M3 · Search · Guidelines](https://m3.material.io/components/search/guidelines).

| ✅ Do | ❌ Don't |
|---|---|
| Use search for products with many items to manage — files, messages | Don't add search to a screen with a handful of items |
| Make the entry point easy to find | Don't bury search behind an ambiguous icon |
| Use a **search bar** to search the contents of a specific view | Don't use a bar when search is the app's global primary function — use an app-bar variant |
| Use a search **icon trigger** when search is a secondary action | Don't take a full bar's space for a rarely-used action |
| Place a search bar below the title for scoped content | Don't detach the bar from the content it searches |
| Show suggestions on focus, results as text is entered | Don't leave the panel empty with no guidance |
| Announce result counts | Don't change results silently for screen-reader users |
| Keep the placeholder descriptive ("Search messages") | Don't ship a bare "Search" when the scope matters |

---

## Patterns

```html
<!-- Docked bar, one query source (mdSearch), consumer-driven loading -->
<md-search id="s" layout="docked" trigger="bar" placeholder="Search messages"
           input-aria-label="Search messages" debounce="300" throttle="1000">
  <md-list slot="results" id="results"></md-list>
</md-search>

<script type="module">
  const s = document.getElementById('s');
  const list = document.getElementById('results');

  s.addEventListener('mdSearch', async (e) => {
    const query = e.detail.value;          // already trimmed and de-duplicated
    if (!query) { list.innerHTML = ''; return; }
    s.loading = true;
    try {
      const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
      const items = await res.json();
      list.innerHTML = items
        .map((i) => `<md-list-item headline="${i.title}"></md-list-item>`)
        .join('');
    } finally {
      s.loading = false;
    }
  });

  s.addEventListener('mdSubmit', (e) => {
    location.href = `/search?q=${encodeURIComponent(e.detail.value)}`;
  });
  s.addEventListener('mdClose', () => { list.innerHTML = ''; });
</script>
```

```html
<!-- Collapsed icon trigger, full-screen overlay -->
<md-search trigger="icon" trigger-icon="search" layout="full-screen"
           placeholder="Search files" open-leading-icon="arrow_back">
  <md-list slot="results"></md-list>
</md-search>
```

```html
<!-- Opener outside the component: an app-bar button drives the search -->
<md-icon-button id="app-bar-search" icon="search" aria-label="Search"></md-icon-button>
<md-search trigger-for="#app-bar-search" layout="docked" placeholder="Search files">
  <md-list slot="results"></md-list>
</md-search>
```

```html
<!-- Voice input: act on the final transcript only -->
<md-search id="v" layout="docked" trigger="bar" voice-search debounce="300">
  <md-list slot="results"></md-list>
</md-search>
<script type="module">
  const v = document.getElementById('v');
  v.addEventListener('mdVoice', (e) => {
    if (e.detail.final) console.log('final transcript:', e.detail.value);
  });
</script>
```

```html
<!-- Divided bar, capped panel, full container width -->
<md-search variant="divided" layout="docked" trigger="bar"
           full-width max-block-size="60vh" elevation="2">
  <md-list slot="results"></md-list>
</md-search>
```

```html
<!-- Localized -->
<md-search placeholder="Rechercher des messages"
           input-aria-label="Rechercher des messages"
           results-label="{count} résultats disponibles"
           no-results-label="Aucun résultat"
           loading-label="Recherche">
  <md-list slot="results"></md-list>
</md-search>
```

## Anti-patterns

| ❌ Wrong | ✅ Right | Why |
|---|---|---|
| Expecting built-in search results | Render into the `results` slot | The component provides the surface only. |
| Wiring `mdInput`, `mdSearch`, `mdSubmit` and `mdChange` to the same query | Fetch on `mdSearch` alone | You would run the query up to four times per keystroke sequence. |
| Setting `debounce` and then listening to `mdInput` | Listen to `mdSearch` | `debounce`/`throttle` gate `mdSearch` only; `mdInput` is always immediate. |
| `throttle` on its own | Pair it with `debounce` | It is a max-wait for the debounce, not a standalone rate limiter. |
| Querying on every `mdVoice` | Wait for `final: true` | Interim transcripts stream continuously. |
| Waiting for the component to clear `loading` | Set `loading` yourself around the fetch | The component never flips it. |
| `triggerElement="..."` as an attribute | `el.triggerElement = ref` in JS | Element references cannot cross the attribute boundary. |
| A built-in trigger **and** `trigger-for` | Pick one | With `trigger-for` set, the component renders no resting affordance. |
| Guarding `voice-search` against a dead mic button | Just set it | The mic is not rendered when the Speech API is missing. |
| Translating `results-label` and dropping `{count}` | Keep the token | The count interpolates into it. |
| `md-search` for filtering a small visible list | `md-text-field type="search"` | An overkill surface for in-place filtering. |
| A search bar as the app's global search | Use an app-bar search variant | M3 explicit rule. |
| Expecting the value in `FormData` | Read `mdSearch` / `mdSubmit` | It is not form-associated. |
| `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** — `input-aria-label` names the input; left empty it falls
back to `placeholder`, so set it explicitly only when the visible hint and the
accessible name should differ. `announce-results` publishes counts to a polite
live region via `results-label` / `no-results-label` — keep it on unless you
manage your own live region inside the `results` slot. `escape-closes` and
`dismiss-on-outside-click` are on by default. `full-screen` traps Tab and locks
document scroll while open; `docked` is non-modal. ArrowDown/ArrowUp move focus
through the slotted results. `initial-focus` controls where focus lands when
the panel opens (`auto`/`input` → the text field, `leading` → the leading
button, falling back to the input). A wired external trigger receives focus
back when the view closes, and carries `aria-haspopup="dialog"` plus a synced
`aria-expanded`. Anything you slot into `results` is **your** responsibility for
roles and keyboard semantics — `md-list` + `md-list-item` is the usual choice.

**RTL** — bar, icons and panel mirror under `dir="rtl"`. Set
`open-leading-icon` to the correct directional glyph for the locale if you
override the default.

**Density** — set `density="-1"` … `density="-4"` for a local override, or let
the bar inherit an ancestor's `data-density` rung; both drive the same
`--md-sys-density-scale` signal, which tapers the bar height, the icon-button
size and the icon size. There is no `density="0"` rule — rung 0 is the
uncompacted default. For dimensions density does not reach, use
`--md-search-container-height` and the padding properties.

**i18n** — translate `placeholder`, `input-aria-label`, `results-label` (keep
the `{count}` token), `no-results-label` and `loading-label`. Voice recognition
takes its language from the page, so set `lang` on `<html>`.

## Related components

`md-autocomplete` · `md-text-field` · `md-app-bar` · `md-icon-button` ·
`md-list` · `md-list-item` · `md-loading-indicator` · `md-menu`

## Theming

| Custom property | Purpose | Default |
|---|---|---|
| `--md-search-container-color` | Bar surface colour | `--md-sys-color-surface-container-high` |
| `--md-search-contained-background-color` | `contained` variant results surface | `--md-sys-color-surface-container-low` |
| `--md-search-container-shape` | Bar corner radius | `--md-sys-shape-corner-full` |
| `--md-search-container-height` | Bar height | density-scaled, `56px` at rung 0 |
| `--md-search-container-min-inline-size` | Docked bar / panel min width | `360px` |
| `--md-search-container-max-inline-size` | Docked bar / panel max width | `720px` |
| `--md-search-container-padding-inline` | Bar inner inline padding | `0px` |
| `--md-search-container-elevation` | Full custom bar shadow — overrides the `elevation` prop | unset (the `elevation` tier) |
| `--md-search-expand-inset` | Resting side gutter of the bar | `24px` |
| `--md-search-expand-focused-inset` | Focused side gutter of the bar | `12px` |
| `--md-search-leading-padding-inline` | Leading cluster inline padding | `4px` |
| `--md-search-trailing-padding-inline` | Trailing cluster inline padding | `8px` |
| `--md-search-icon-size` | Leading / trailing glyph size | density-scaled, `24px` at rung 0 |
| `--md-search-icon-button-size` | Leading / trailing hit target | density-scaled, `48px` at rung 0 |
| `--md-search-icon-color` | Fallback ink for the trailing icon | `--md-sys-color-on-surface-variant` |
| `--md-search-leading-icon-color` | Leading icon ink | `--md-sys-color-on-surface` |
| `--md-search-trailing-icon-color` | Trailing icon ink | `--md-search-icon-color`, else `--md-sys-color-on-surface-variant` |
| `--md-search-input-color` | Input text colour | `--md-sys-color-on-surface` |
| `--md-search-placeholder-color` | Placeholder colour | `--md-sys-color-on-surface-variant` |
| `--md-search-input-font-size` | Input type size | density-scaled, `16px` at rung 0 |
| `--md-search-panel-min-block-size` | Results panel minimum height | `0px` |
| `--md-search-panel-max-block-size` | Results panel maximum height | `min(400px, 60vh)` |
| `--md-search-max-block-size` | Cap on the whole open surface (same as `max-block-size`) | unset |
| `--md-search-panel-offset` | Gap between the docked bar and its panel | focus-ring offset + thickness + `3px` |
| `--md-search-docked-panel-shape` | Docked panel corner radius | `--md-sys-shape-corner-large` (16px) |
| `--md-search-divider-color` | `divided` variant rule colour | `--md-sys-color-outline` |
| `--md-search-divider-thickness` | `divided` variant rule thickness | `1px` |
| `--md-search-focus-indicator-color` | Keyboard focus ring colour | `--md-sys-color-secondary` |
| `--md-search-focus-indicator-thickness` | Keyboard focus ring thickness | `3px` |
| `--md-search-focus-indicator-offset` | Keyboard focus ring offset | `2px` |
| `--md-search-loading-color` | In-bar loading indicator ink | `--md-sys-color-primary` |
| `--md-search-empty-color` | Empty-state text colour | `--md-sys-color-on-surface-variant` |
| `--md-search-empty-padding-block` | Empty-state block padding | `--md-sys-spacing-inset-xl` (24px) |
| `--md-search-empty-padding-inline` | Empty-state inline padding | `--md-sys-spacing-inset-lg` (16px) |
| `--md-search-expand-easing` | Bar expand/collapse easing | `cubic-bezier(0.34, 1.56, 0.64, 1)` |
| `--md-search-expand-duration` | Bar expand/collapse duration | see stylesheet |
| `--md-search-fullscreen-expand-duration` | Full-screen open duration | see stylesheet |
| `--md-search-fullscreen-collapse-duration` | Full-screen close duration | see stylesheet |

**CSS parts** — `trigger`, `trigger-button`, `bar`, `state-layer`, `leading`,
`leading-state-layer`, `input`, `clear-button`, `voice-button`, `trailing`,
`loading`, `status`, `panel`, `divider`, `panel-body`, `empty`,
`results-host`, `results-viewport`.

```css
md-search {
  --md-search-container-max-inline-size: 560px;
  --md-search-panel-max-block-size: 50vh;
}
md-search::part(panel-body) {
  padding-block-end: 8px;
}
```

<!-- Auto Generated Below -->


## Overview

Material Design 3 Search.

Implements the M3 Search specification
(https://m3.material.io/components/search/specs).

Two orthogonal axes describe every spec configuration:
 - `variant`: `contained` (M3 Expressive default) | `divided` (baseline)
 - `layout` : `full-screen` (modal overlay) | `docked` (anchored panel)

The component renders a single resting bar plus a results panel. When the
`full-screen` layout is `open` the bar pins itself to the top of the
viewport via `position: fixed` so consumers don't need a portal — CSS does
the work, and the input keeps a stable identity throughout the open/close
lifecycle.

## Properties

| Property                | Attribute                  | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | Type                             | Default                       |
| ----------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | ----------------------------- |
| `announceResults`       | `announce-results`         | Announce the number of available suggestions / results to assistive technology via a polite live region whenever the slotted results change while the panel is open (M3 "autosuggest" requirement). Set to `false` if you manage your own live region in the results slot.                                                                                                                                                                                                                                                      | `boolean`                        | `true`                        |
| `debounce`              | `debounce`                 | Trailing debounce (ms) applied to the `mdSearch` event so a fetch only fires after the user pauses typing. `0` (default) emits on every change. `mdInput` always fires immediately on every keystroke regardless.                                                                                                                                                                                                                                                                                                               | `number`                         | `0`                           |
| `density`               | `density`                  | Local density rung. Drives the same `--md-sys-density-scale` signal that a global `data-density` ancestor sets, so a local value simply overrides the inherited one. 0 = default, -4 = ultra-compact.                                                                                                                                                                                                                                                                                                                           | `-1 \| -2 \| -3 \| -4 \| 0`      | `0`                           |
| `disabled`              | `disabled`                 | Whether interaction is blocked.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | `boolean`                        | `false`                       |
| `dismissOnOutsideClick` | `dismiss-on-outside-click` | Close on outside click (docked layout).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `boolean`                        | `true`                        |
| `elevation`             | `elevation`                | Resting bar elevation level, mapped to `--md-sys-elevation-{n}`. `0` (the M3 default) renders no shadow; `1`–`5` apply the matching MD3 elevation tier. For a fully custom shadow, set the CSS custom property `--md-search-container-elevation` instead (it overrides this prop).                                                                                                                                                                                                                                              | `0 \| 1 \| 2 \| 3 \| 4 \| 5`     | `0`                           |
| `escapeCloses`          | `escape-closes`            | Close on Escape key.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | `boolean`                        | `true`                        |
| `fullWidth`             | `full-width`               | Make the docked / inline bar span its container's full inline width with NO side gutters. This zeroes both expand insets for the instance (resting + focused = 0), so there is no 24 → 12px margin animation and the aligned results drawer fills the same full width. Purely an ergonomic switch over the CSS vars — the identical effect is available by setting `--md-search-expand-inset: 0` and `--md-search-expand-focused-inset: 0`. Has no effect on `full-screen` layout (already edge-to-edge).                       | `boolean`                        | `false`                       |
| `initialFocus`          | `initial-focus`            | Where focus lands when the panel opens. `auto` (default) and `input` focus the text field so the user can type immediately. `leading` focuses the leading button instead (falling back to the input if it isn't focusable).  Note: the M3 "initial focus lands on a leading icon, else the text field" rule applies to the RESTING bar's first tab stop and is handled by DOM order — a slotted interactive leading icon button is the first tabbable element, otherwise the text field. This prop only controls the open view. | `"auto" \| "input" \| "leading"` | `'auto'`                      |
| `inputAriaLabel`        | `input-aria-label`         | Accessible label for the search bar / input. Per the M3 labeling guidance the hinted search text (`placeholder`) describes the bar, so when this is left empty the label falls back to `placeholder`. Set it explicitly only when the visible hint and the accessible name should differ.                                                                                                                                                                                                                                       | `string`                         | `''`                          |
| `layout`                | `layout`                   | How the focused panel appears. `full-screen` fills the viewport edge-to-edge with an opaque results surface and a focus trap (no scrim — open/close uses the same translateY + opacity motion as md-dialog fullscreen); `docked` anchors a popup beneath the bar (min 360px / max 720px wide; results drawer shrinks to content up to max ⅔ vh).                                                                                                                                                                                | `"docked" \| "full-screen"`      | `'full-screen'`               |
| `leadingIcon`           | `leading-icon`             | Material Symbols glyph for the resting leading icon.                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | `string`                         | `'search'`                    |
| `loading`               | `loading`                  | Whether an async results fetch is in flight. When `true` the M3 loading indicator (the looping shape-morph) shows in the bar's trailing cluster. Controlled by the consumer: set it `true` before awaiting your fetch and back to `false` once results are slotted in. The component never flips this itself — it only reflects the state visually + to assistive tech.                                                                                                                                                         | `boolean`                        | `false`                       |
| `loadingLabel`          | `loading-label`            | Accessible label for the in-bar loading indicator.                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | `string`                         | `'Searching'`                 |
| `maxBlockSize`          | `max-block-size`           | Maximum block-size of the open search surface (`"480px"`, `"66vh"`, etc.). Full-screen: caps the fixed overlay height. Docked: caps the results panel (bar height is unchanged). Prefer the CSS custom property `--md-search-max-block-size` for stylesheet-based theming.                                                                                                                                                                                                                                                      | `string`                         | `''`                          |
| `noResultsLabel`        | `no-results-label`         | Message shown when the panel is open, the user has entered a query, and there are no slotted results — both the on-screen empty state and the polite live-region announcement (unless the visible empty state is shown, in which case the live region defers to it to avoid duplicate speech).                                                                                                                                                                                                                                  | `string`                         | `'No results available'`      |
| `open`                  | `open`                     | Whether the focused panel is open.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | `boolean`                        | `false`                       |
| `openLeadingIcon`       | `open-leading-icon`        | Material Symbols glyph used for the leading button when the bar is open. Defaults are variant-aware: the `contained` (Expressive) bar uses `chevron_left` so the caret matches the bar's softer, pill- shaped silhouette, while the `divided` (baseline) bar uses the classic `arrow_back` to match its squared, app-bar-like chrome. Pass an explicit value to override either default.                                                                                                                                        | `string \| undefined`            | `undefined`                   |
| `placeholder`           | `placeholder`              | Placeholder text — the spec calls this "supporting text" within the bar anatomy.                                                                                                                                                                                                                                                                                                                                                                                                                                                | `string`                         | `'Search'`                    |
| `resultsLabel`          | `results-label`            | Template for the results announcement. `{count}` is replaced with the number of detected result items (counts `md-list-item`, `[role="option"]`, or `<li>` descendants of the results slot, falling back to the number of slotted elements).                                                                                                                                                                                                                                                                                    | `string`                         | `'{count} results available'` |
| `scrollShadow`          | `scroll-shadow`            | Retained for API compatibility, but now a no-op: when the results list overflows it always plain-scrolls inside the results viewport (no scroll shadow / edge fades). Setting this has no visual effect.                                                                                                                                                                                                                                                                                                                        | `boolean`                        | `true`                        |
| `showClearButton`       | `show-clear-button`        | Show the trailing clear (×) button when the input has a value.                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | `boolean`                        | `true`                        |
| `throttle`              | `throttle`                 | Maximum wait (ms) before `mdSearch` is forced to fire during sustained typing — a throttle/`maxWait` safety net so the user still sees interim results when they never pause long enough for the debounce to settle. `0` (default) disables it. Only meaningful alongside a non-zero `debounce`.                                                                                                                                                                                                                                | `number`                         | `0`                           |
| `trigger`               | `trigger`                  | What the user sees while the search view is closed. `bar` renders the resting search field (docked default). `icon` renders a compact search icon button that opens the view — the default for `full-screen` layout (contained and divided spec sheets). Docked layouts should keep `bar`.                                                                                                                                                                                                                                      | `"bar" \| "icon" \| undefined`   | `undefined`                   |
| `triggerElement`        | --                         | The external trigger as an element, for consumers that hold a reference (frameworks that pass a ref, or a button rendered in another shadow root where a document selector can't reach). Takes precedence over `trigger-for`.                                                                                                                                                                                                                                                                                                    | `HTMLElement \| undefined`       | `undefined`                   |
| `triggerFor`            | `trigger-for`              | Document selector for an opener that lives OUTSIDE this element — an app-bar button, a menu item, a shortcut hint. The component wires it rather than rendering it: activation toggles the view, `aria-haspopup="dialog"` and `aria-expanded` are kept in step, and closing returns focus to it. With one set the built-in trigger is not rendered and nothing shows at rest, so the page never offers two ways in. Use a real button — activation rides on `click`.                                                             | `string`                         | `''`                          |
| `triggerIcon`           | `trigger-icon`             | Material Symbols glyph for the built-in icon trigger (`trigger="icon"`). Ignored when a custom element is slotted in `trigger`.                                                                                                                                                                                                                                                                                                                                                                                                 | `string`                         | `'search'`                    |
| `value`                 | `value`                    | Current input text, two-way bindable.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           | `string`                         | `''`                          |
| `variant`               | `variant`                  | Visual style. `contained` is the M3 Expressive default with a filled surface and rounded shape; `divided` is the baseline pre-Expressive look that uses a hairline divider between bar and results.                                                                                                                                                                                                                                                                                                                             | `"contained" \| "divided"`       | `'contained'`                 |
| `voiceSearch`           | `voice-search`             | Opt-in voice search. When `true` AND the browser exposes the Web Speech API (`SpeechRecognition` / `webkitSpeechRecognition`), a microphone icon-button is rendered in the trailing cluster. Clicking it starts recognition and streams the interim + final transcript into the input, firing the normal `mdInput` / `mdSearch` flow as if typed. When the API is unavailable the mic is not rendered (graceful no-op). Default `false`.                                                                                        | `boolean`                        | `false`                       |


## Events

| Event                 | Description                                                                                                                                                                                                                                                                                                                                                               | Type                                              |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
| `mdChange`            | Fires when the input loses focus with a different value than on focus.                                                                                                                                                                                                                                                                                                    | `CustomEvent<{ value: string; }>`                 |
| `mdClear`             | Fires when the user clicks the clear (×) button.                                                                                                                                                                                                                                                                                                                          | `CustomEvent<void>`                               |
| `mdClose`             | Fires when the panel closes.                                                                                                                                                                                                                                                                                                                                              | `CustomEvent<void>`                               |
| `mdInput`             | Fires on every input change (immediate, every keystroke).                                                                                                                                                                                                                                                                                                                 | `CustomEvent<{ value: string; }>`                 |
| `mdLeadingIconClick`  | Fires when the interactive leading affordance is clicked — the morphing back / dismiss button while the bar is open, or a custom slotted `leading` icon. The default back button still dismisses the panel as before; this event is additive. The closed resting search glyph is intentionally NOT a click target (a click there focuses the input), so it does not emit. | `CustomEvent<MdSearchLeadingIconClickDetail>`     |
| `mdOpen`              | Fires when the panel opens.                                                                                                                                                                                                                                                                                                                                               | `CustomEvent<void>`                               |
| `mdSearch`            | Debounced, de-duplicated query event — the one to wire an async results fetch to. Honours `debounce` / `throttle` and applies a distinct-until-changed guard on the trimmed query (so re-typing the same term, or only adding surrounding whitespace, won't re-trigger a fetch). Pressing Enter or clearing the field flushes it immediately.                             | `CustomEvent<{ value: string; }>`                 |
| `mdSubmit`            | Fires when the user presses Enter to submit a query.                                                                                                                                                                                                                                                                                                                      | `CustomEvent<{ value: string; }>`                 |
| `mdTrailingIconClick` | Fires when a slotted `trailing` affordance is clicked. The built-in clear (×) and voice (mic) buttons own their dedicated `mdClear` / `mdVoice` events and do NOT trigger this. Only emitted when slotted trailing content is actually present and was the click target.                                                                                                  | `CustomEvent<MdSearchTrailingIconClickDetail>`    |
| `mdVoice`             | Fires while voice search is active as the transcript streams in. `value` is the current (interim or final) transcript; `final` is `true` on the recognised final result. The component already mirrors the transcript into the input and fires `mdInput` / `mdSearch`, so listen to this only if you need voice-specific UI (e.g. a transcript preview).                  | `CustomEvent<{ value: string; final: boolean; }>` |


## Methods

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

Close the focused panel.

#### Returns

Type: `Promise<void>`



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

Programmatically focus the input.

#### Returns

Type: `Promise<void>`



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

Open the focused panel.

#### Returns

Type: `Promise<void>`



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

Start streaming voice transcription into the input. No-op if unsupported.

#### Returns

Type: `Promise<void>`



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

Stop any active voice transcription.

#### Returns

Type: `Promise<void>`



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

Toggle the focused panel.

#### Returns

Type: `Promise<void>`




## Shadow Parts

| Part                    | Description |
| ----------------------- | ----------- |
| `"bar"`                 |             |
| `"clear-button"`        |             |
| `"divider"`             |             |
| `"empty"`               |             |
| `"input"`               |             |
| `"leading"`             |             |
| `"leading-state-layer"` |             |
| `"loading"`             |             |
| `"panel"`               |             |
| `"panel-body"`          |             |
| `"results-host"`        |             |
| `"results-viewport"`    |             |
| `"state-layer"`         |             |
| `"status"`              |             |
| `"trailing"`            |             |
| `"trigger"`             |             |
| `"trigger-button"`      |             |
| `"voice-button"`        |             |


## Dependencies

### Depends on

- [md-icon-button](../md-icon-button)
- [md-loading-indicator](../md-loading-indicator)
- [md-ripple](../md-ripple)

### Graph
```mermaid
graph TD;
  md-search --> md-icon-button
  md-search --> md-loading-indicator
  md-search --> md-ripple
  md-icon-button --> md-ripple
  style md-search fill:#f9f,stroke:#333,stroke-width:4px
```

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

*Built with [StencilJS](https://stenciljs.com/)*
# 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.