Skip to content

App Bar

The screen’s header. A title, a leading navigation affordance, and a small number of trailing actions — in small / medium / large heights, plus a dedicated search variant for apps where search is the primary global function.

Live preview Open in Storybook
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-app-bar headline="Inbox" subtitle="12 unread" leading-icon="menu" leading-icon-label="Open navigation">
  <md-icon-button slot="trailing" icon="search" variant="standard" size="md" button-width="narrow" aria-label="Search"></md-icon-button>
  <md-icon-button slot="trailing" icon="more_vert" variant="standard" size="md" button-width="narrow" aria-label="More options"></md-icon-button>
</md-app-bar>

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


<md-app-bar></md-app-bar>
  • The top of a screen: current context, a back or menu affordance, and a small number of actions.
  • A taller header for a prominent page title (medium / large).
  • Global search as the primary function (variant="search").
SituationUse instead
Local, page-specific action clustersmd-toolbar
Top-level destinationsmd-navigation-bar / md-navigation-rail
Sibling views inside a screenmd-tabs
Search scoped to one viewmd-search
Where-am-I hierarchymd-breadcrumbs
A row of related actionsmd-button-group
NeedSetting
Standard screen headervariant="small" (default)
A prominent page titlevariant="medium" or "large"
Search as the app’s main jobvariant="search"
Title centred in the rowtitle-alignment="center"
Recolour on scrollscrolledyou set it
Tighter rowsdensity="-1…-4"

small (the default) is a single 64px row. medium and large add an expanded title block below the action row — M3 allows their headline to wrap to two lines maximum. small must never wrap.

VariantShapeHeight (no subtitle → with)Use for
smallOne row, inline title64pxDefault. Standard screen header
mediumRow + expanded title112px → 136pxA prominent page title
largeRow + taller expanded title120px → 152pxThe most prominent page title
searchRow with an inline search pill64pxSearch is the app’s primary function
Small, medium and large Open in Storybook
Show code for each technology
<!-- index.html <head> — the icon font the components draw from -->
<link rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">

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

<div style="display: flex; flex-direction: column; gap: 24px; inline-size: 100%;">
  <md-app-bar headline="Small" leading-icon="arrow_back" leading-icon-label="Back">
    <md-icon-button slot="trailing" icon="more_vert" variant="standard" size="md" button-width="narrow" aria-label="More options"></md-icon-button>
  </md-app-bar>

  <md-app-bar variant="medium" headline="Medium headline" leading-icon="arrow_back" leading-icon-label="Back">
    <md-icon-button slot="trailing" icon="more_vert" variant="standard" size="md" button-width="narrow" aria-label="More options"></md-icon-button>
  </md-app-bar>

  <md-app-bar variant="large" headline="Quarterly performance review" subtitle="Updated 2 minutes ago" leading-icon="arrow_back" leading-icon-label="Back">
    <md-icon-button slot="trailing" icon="more_vert" variant="standard" size="md" button-width="narrow" aria-label="More options"></md-icon-button>
  </md-app-bar>
</div>
Medium — the subtitle adds 24px Open in Storybook
Show code for each technology
<!-- index.html <head> — the icon font the components draw from -->
<link rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">

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

<div style="display: flex; flex-direction: column; gap: 24px; inline-size: 100%;">
  <md-app-bar variant="medium" headline="Medium, no subtitle" leading-icon="arrow_back" leading-icon-label="Back">
    <md-icon-button slot="trailing" icon="more_vert" variant="standard" size="md" button-width="narrow" aria-label="More options"></md-icon-button>
  </md-app-bar>

  <md-app-bar variant="medium" headline="Medium, with subtitle" subtitle="24px taller" leading-icon="arrow_back" leading-icon-label="Back">
    <md-icon-button slot="trailing" icon="more_vert" variant="standard" size="md" button-width="narrow" aria-label="More options"></md-icon-button>
  </md-app-bar>
</div>
Large — the subtitle adds 32px Open in Storybook
Show code for each technology
<!-- index.html <head> — the icon font the components draw from -->
<link rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">

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

<div style="display: flex; flex-direction: column; gap: 24px; inline-size: 100%;">
  <md-app-bar variant="large" headline="Large, no subtitle" leading-icon="arrow_back" leading-icon-label="Back">
    <md-icon-button slot="trailing" icon="more_vert" variant="standard" size="md" button-width="narrow" aria-label="More options"></md-icon-button>
  </md-app-bar>

  <md-app-bar variant="large" headline="Large, with subtitle" subtitle="32px taller" leading-icon="arrow_back" leading-icon-label="Back">
    <md-icon-button slot="trailing" icon="more_vert" variant="standard" size="md" button-width="narrow" aria-label="More options"></md-icon-button>
  </md-app-bar>
</div>

title-alignment="center" centres the headline in the action row. It has no effect on the expanded title block of medium / large.

Title alignment
Show code for each technology
<!-- index.html <head> — the icon font the components draw from -->
<link rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">

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

<div style="display: flex; flex-direction: column; gap: 24px; inline-size: 100%;">
  <md-app-bar headline="Start aligned" leading-icon="menu" leading-icon-label="Open navigation">
    <md-icon-button slot="trailing" icon="more_vert" variant="standard" size="md" button-width="narrow" aria-label="More options"></md-icon-button>
  </md-app-bar>

  <md-app-bar title-alignment="center" headline="Centre aligned" leading-icon="menu" leading-icon-label="Open navigation">
    <md-icon-button slot="trailing" icon="more_vert" variant="standard" size="md" button-width="narrow" aria-label="More options"></md-icon-button>
  </md-app-bar>
</div>

variant="search" swaps the title row for an inline search pill. The M3 composition is a leading affordance, an in-pill trailing action via the search-trailing slot, and an account affordance in trailing.

Search bars — type in the pill to fire mdSearchInput Open in Storybook
Show code for each technology
<!-- index.html <head> — the icon font the components draw from -->
<link rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">

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

<div style="display: flex; flex-direction: column; gap: 24px; inline-size: 100%;">
  <md-app-bar variant="search" search-placeholder="Search mail" search-aria-label="Search mail" leading-icon="menu" leading-icon-label="Open navigation">
    <md-icon-button slot="search-trailing" icon="mic" variant="standard" size="md" button-width="narrow" aria-label="Voice search"></md-icon-button>
    <md-avatar slot="trailing" name="Ada Lovelace" label="Account"></md-avatar>
  </md-app-bar>

  <md-app-bar variant="search" title-alignment="center" search-placeholder="Search mail" search-aria-label="Search mail" leading-icon="menu" leading-icon-label="Open navigation">
    <md-icon-button slot="search-trailing" icon="mic" variant="standard" size="md" button-width="narrow" aria-label="Voice search"></md-icon-button>
    <md-avatar slot="trailing" name="Ada Lovelace" label="Account"></md-avatar>
  </md-app-bar>
</div>

search-value is the field’s value and reflects as you type. search-disabled greys the pill out and makes it inert: clicks, focus and Enter / Space are all ignored, no mdSearchActivate fires, and the pill never takes the focused state.

A pre-filled value, and a disabled pill Open in Storybook
Show code for each technology
<!-- index.html <head> — the icon font the components draw from -->
<link rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">

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

<div style="display: flex; flex-direction: column; gap: 24px; inline-size: 100%;">
  <md-app-bar variant="search" search-value="quarterly report" search-aria-label="Search mail" leading-icon="menu" leading-icon-label="Open navigation">
    <md-icon-button slot="search-trailing" icon="close" variant="standard" size="md" button-width="narrow" aria-label="Clear search"></md-icon-button>
  </md-app-bar>

  <md-app-bar variant="search" search-disabled search-placeholder="Search (disabled)" search-aria-label="Search mail" leading-icon="menu" leading-icon-label="Open navigation">
    <md-icon-button slot="search-trailing" icon="mic" variant="standard" size="md" button-width="narrow" aria-label="Voice search"></md-icon-button>
  </md-app-bar>
</div>

The inline input derives its accessible name from a three-step fallback:

OrderSourceNotes
1search-aria-labelThe only one you should rely on
2search-placeholderOnly when search-aria-label is empty
3The literal SearchWhen both are empty — an untranslated last resort

Set search-aria-label explicitly. Falling through to the placeholder ties the accessible name to a hint that may change for visual reasons, and step 3 ships English into every locale.

Named field, and one falling through to the literal Search Open in Storybook
Show code for each technology
<!-- index.html <head> — the icon font the components draw from -->
<link rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">

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

<div style="display: flex; flex-direction: column; gap: 24px; inline-size: 100%;">
  <md-app-bar variant="search" search-aria-label="Search all mail" search-placeholder="Type here" leading-icon="menu" leading-icon-label="Open navigation"></md-app-bar>

  <md-app-bar variant="search" search-placeholder="" leading-icon="menu" leading-icon-label="Open navigation"></md-app-bar>
</div>

The search slot swaps out the built-in <input> entirely — for a combobox, a framework-controlled input, or an autocomplete. The pill, its state layer and the search-trailing bay stay; only the field is yours.

Slotted search field — the bar keeps the pill, you own the input Open in Storybook
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-app-bar variant="search" leading-icon="menu" leading-icon-label="Open navigation">
  <input slot="search" aria-label="Custom search field" placeholder="Your own input" style="inline-size:100%;border:0;background:none;outline:none;font:inherit;color:inherit;" />
  <md-icon-button slot="search-trailing" icon="mic" variant="standard" size="md" button-width="narrow" aria-label="Voice search"></md-icon-button>
</md-app-bar>

The search row responds to its own container width, not just the viewport:

Container widthBehaviour
≥ 600pxHint inset 24px, every search-trailing icon visible
< 600pxHint inset 16px, second and later search-trailing icons hidden
< 400pxHint inset 12px

Add data-compact-keep to a search-trailing child to exempt it from the compact hide. Row gaps and the 48px touch targets never shrink.

Both frames are under 600px — the tune icon is hidden in both, only the hint inset differs Open in Storybook
Show code for each technology
<!-- index.html <head> — the icon font the components draw from -->
<link rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">

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

<div style="display:grid;gap:20px;inline-size:100%;">
  <div style="inline-size:min(100%,320px);">
    <md-app-bar variant="search" search-placeholder="320px — 12px hint" search-aria-label="Search" leading-icon="menu" leading-icon-label="Open navigation">
      <md-icon-button slot="search-trailing" icon="mic" variant="standard" size="md" button-width="narrow" aria-label="Voice search"></md-icon-button>
      <md-icon-button slot="search-trailing" data-compact-keep icon="auto_awesome" variant="standard" size="md" button-width="narrow" aria-label="AI search"></md-icon-button>
      <md-icon-button slot="search-trailing" icon="tune" variant="standard" size="md" button-width="narrow" aria-label="Search filters"></md-icon-button>
    </md-app-bar>
  </div>
  <div style="inline-size:min(100%,520px);">
    <md-app-bar variant="search" search-placeholder="~510px — 16px hint" search-aria-label="Search" leading-icon="menu" leading-icon-label="Open navigation">
      <md-icon-button slot="search-trailing" icon="mic" variant="standard" size="md" button-width="narrow" aria-label="Voice search"></md-icon-button>
      <md-icon-button slot="search-trailing" data-compact-keep icon="auto_awesome" variant="standard" size="md" button-width="narrow" aria-label="AI search"></md-icon-button>
      <md-icon-button slot="search-trailing" icon="tune" variant="standard" size="md" button-width="narrow" aria-label="Search filters"></md-icon-button>
    </md-app-bar>
  </div>
</div>
Resting vs scrolled container colour
Show code for each technology
<!-- index.html <head> — the icon font the components draw from -->
<link rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">

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

<div style="display: flex; flex-direction: column; gap: 24px; inline-size: 100%;">
  <md-app-bar headline="At rest" leading-icon="menu" leading-icon-label="Open navigation">
    <md-icon-button slot="trailing" icon="more_vert" variant="standard" size="md" button-width="narrow" aria-label="More options"></md-icon-button>
  </md-app-bar>

  <md-app-bar scrolled headline="Scrolled" leading-icon="menu" leading-icon-label="Open navigation">
    <md-icon-button slot="trailing" icon="more_vert" variant="standard" size="md" button-width="narrow" aria-label="More options"></md-icon-button>
  </md-app-bar>
</div>
const bar = document.getElementById('bar');
addEventListener('scroll', () => { bar.scrolled = window.scrollY > 0; }, { passive: true });

On a search bar the pill recolours too — surface-container-highest at rest, surface-container when scrolled — so the field stays distinguishable from the row behind it in both states.

SlotHoldsNotes
leadingThe navigation affordanceReplaces the leading-icon prop entirely
headlineCustom title contentUsed only when the headline prop is empty
subtitleCustom subtitle contentUsed only when the subtitle prop is empty
trailingAction icons, avatarMax 3 — extras are hidden
searchA replacement search fieldsearch variant only
search-trailingIn-pill icons (mic, clear)data-compact-keep survives the narrow-container hide
Slotted headline, subtitle and trailing content — no headline prop set
Inbox 12 unread
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-app-bar leading-icon="menu" leading-icon-label="Open navigation">
  <span slot="headline">Inbox</span>
  <span slot="subtitle">12 unread</span>
  <md-icon-button slot="trailing" icon="filter_list" variant="standard" size="md" button-width="narrow" aria-label="Filter"></md-icon-button>
  <md-avatar slot="trailing" name="Ada Lovelace" label="Account"></md-avatar>
</md-app-bar>

Flexible bars project slotted content into the expanded block the same way:

Slotted expanded headline on a medium bar Open in Storybook
Slotted headline Slotted subtitle
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-app-bar variant="medium" leading-icon="arrow_back" leading-icon-label="Back">
  <span slot="headline">Slotted headline</span>
  <span slot="subtitle">Slotted subtitle</span>
  <md-icon-button slot="trailing" icon="more_vert" variant="standard" size="md" button-width="narrow" aria-label="More options"></md-icon-button>
</md-app-bar>

The trailing slot renders at most 3 children. Extras are hidden and a warning is logged in development. Removing a visible one re-reveals a hidden one — the bar re-syncs on slotchange.

Trailing icon limit — the 4th and 5th are hidden Open in Storybook
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-app-bar headline="Five requested, three rendered" leading-icon="menu" leading-icon-label="Open navigation">
  <md-icon-button slot="trailing" icon="search" variant="standard" size="md" button-width="narrow" aria-label="Search"></md-icon-button>
  <md-icon-button slot="trailing" icon="filter_list" variant="standard" size="md" button-width="narrow" aria-label="Filter"></md-icon-button>
  <md-icon-button slot="trailing" icon="more_vert" variant="standard" size="md" button-width="narrow" aria-label="More options"></md-icon-button>
  <md-icon-button slot="trailing" icon="delete" variant="standard" size="md" button-width="narrow" aria-label="Delete"></md-icon-button>
  <md-icon-button slot="trailing" icon="archive" variant="standard" size="md" button-width="narrow" aria-label="Archive"></md-icon-button>
</md-app-bar>

This is a guardrail, not a bug: M3 caps an app bar at three trailing actions. Past three, move the rest behind an overflow md-menu.

Slot md-icon-button (or a raw SVG) into leading, trailing or search-trailing when the Material Symbols name isn’t enough — a brand glyph, an icon font, a logo. Host ::slotted rules map the icon button onto the bar’s 48px touch target for you.

Slotted SVG glyphs in leading and trailing 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-app-bar headline="Custom glyphs">
  <md-icon-button slot="leading" variant="standard" size="md" button-width="narrow" aria-label="Home">
    <svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor"><path d="M12 3l9 8h-3v10h-5v-6h-2v6H6V11H3z"/></svg>
  </md-icon-button>
  <md-icon-button slot="trailing" variant="standard" size="md" button-width="narrow" aria-label="Starred">
    <svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01z"/></svg>
  </md-icon-button>
  <md-avatar slot="trailing" name="Ada Lovelace" label="Account"></md-avatar>
</md-app-bar>
EventCancelableDetailFires
mdLeadingClicknoMouseEventThe leading affordance is activated
mdSearchInputno{ value: string }Every keystroke in the search pill
mdSearchActivateno{ value: string }The search pill is focused, or Enter/Space is pressed in it
You want to…Listen to
Open a navigation drawer or go backmdLeadingClick
Update suggestions as the user typesmdSearchInput
Run the querymdSearchActivate
React to a slotted search fieldYour own element’s events — not these

The bar reports the leading press; it does not open a drawer or navigate. That wiring is yours — the affordance below is a real button that emits the event and does nothing else.

mdLeadingClick — open in Storybook to watch it fire Open in Storybook
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-app-bar headline="Press the menu icon" leading-icon="menu" leading-icon-label="Open navigation">
  <md-icon-button slot="trailing" icon="more_vert" variant="standard" size="md" button-width="narrow" aria-label="More options"></md-icon-button>
</md-app-bar>
<md-app-bar id="bar" variant="search" headline="Inbox"
          leading-icon="menu" leading-icon-label="Open navigation"
          search-placeholder="Search mail" search-aria-label="Search mail">
</md-app-bar>

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

bar.addEventListener('mdLeadingClick', () => rail.toggle());
bar.addEventListener('mdSearchInput', (e) => suggest(e.detail.value));
bar.addEventListener('mdSearchActivate', (e) => runSearch(e.detail.value));

addEventListener('scroll', () => { bar.scrolled = window.scrollY > 0; }, { passive: true });
</script>

Properties

PropertyAttributeTypeDefaultReflects
variantvariant'small' | 'medium' | 'large' | 'search''small'Yes
titleAlignmenttitle-alignment'start' | 'center''start'Yes
searchPlaceholdersearch-placeholderstring'Search'
searchValuesearch-valuestring''Yes
searchAriaLabelsearch-aria-labelstring''
searchDisabledsearch-disabledbooleanfalseYes
headlineheadlinestring''Yes
subtitlesubtitlestring''Yes
scrolledscrolledbooleanfalseYes
densitydensity0 | -1 | -2 | -3 | -40Yes
leadingIconleading-iconstring''
leadingIconLabelleading-icon-labelstring''

Slots

SlotDescription
headlineCustom headline content (overrides headline prop)
subtitleCustom subtitle content (overrides subtitle prop)
searchReplace built-in search input (search configuration)
search-trailingIn-pill trailing icons (e.g. mic); data-compact-keep opts out of compact hide
leadingLeading navigation icon(s) — slot takes priority over leading-icon prop
trailingTrailing action icons outside search field (max 3; excess hidden + warn)

CSS Custom Properties

Override on the host element for per-instance theming:

PropertyDescription
--md-app-bar-container-colorTop bar background (flat → surface)
--md-app-bar-container-color-scrolledTop bar background when scrolled (surface-container)
--md-app-bar-container-shapeTop bar container corner radius (none)
--md-app-bar-headline-colorHeadline / title colour (on-surface)
--md-app-bar-subtitle-colorSubtitle colour (on-surface-variant)
--md-app-bar-leading-icon-colorLeading icon colour (on-surface)
--md-app-bar-trailing-icon-colorTrailing icon colour (on-surface-variant)
--md-app-bar-container-elevationContainer shadow (elevation-0 default)
--md-app-bar-row-heightAction row height (64px)
--md-app-bar-padding-inline-startAction row inline-start padding (4px)
--md-app-bar-padding-inline-endAction row inline-end padding (4px)
--md-app-bar-row-padding-inlineDeprecated alias for both paddings (4px)
--md-app-bar-row-padding-block-startAction row block-start padding (20px large expanded)
--md-app-bar-trailing-gapGap between trailing action icons (2px)
--md-app-bar-trailing-icon-touch-sizeTrailing icon touch target (48px; matches leading)
--md-app-bar-icon-gapGap between icons and row content (0)
--md-app-bar-icon-sizeIcon glyph size in action row (24px,
--md-app-bar-icon-button-size
--md-app-bar-avatar-size
--md-app-bar-expanded-padding-inline
--md-app-bar-expanded-padding-block-end
--md-app-bar-search-container-height
--md-app-bar-search-padding-inline-start
--md-app-bar-search-padding-inline-end
--md-app-bar-search-row-gap
--md-app-bar-search-trailing-gap
--md-app-bar-search-field-trailing-gap
--md-app-bar-trailing-touch-size
--md-app-bar-medium-height
--md-app-bar-medium-height-with-subtitle
--md-app-bar-large-height
--md-app-bar-large-height-with-subtitle

CSS Shadow Parts

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

PartDescription
leading-iconProp-based leading md-icon-button
titleInline title (small variant)
subtitleSubtitle text (inline on small, expanded on medium/large)
expanded-headlineExpanded headline text (headline-medium)
search-hostFlex region between leading/trailing (search variant)
searchSearch pill container (search variant)
search-state-layerHover / focus / press overlay on search field
search-fieldText/input flex region inside pill (hint padding)
search-contentWrapper around slotted `search` content
search-inputInline search `<input>` (search variant)
search-trailingTrailing slot inside the search field
rowTop action row (leading | title | trailing)
leadingLeading slot wrapper
trailingTrailing slot wrapper (outside search field)
expandedExpanded title block wrapper (medium / large)
iconLeading icon glyph (exported from leading-icon)
state-layerLeading icon state layer (exported from leading-icon)
  • Give the leading affordance a localized leading-icon-label — it is an icon-only control and needs an accessible name.
  • Every trailing md-icon-button needs its own aria-label.
  • The headline’s styling is not a heading. Slot a real heading element, or provide one elsewhere in the document outline.
  • For the search variant set search-aria-label; the fallback chain ends at an untranslated Search.
  • On medium / large the collapsed inline title is aria-hidden — the expanded headline is the one announced, so the title is never read twice.
  • A fixed app bar covers content — add matching top padding to the page.
KeyAction
TabLeading affordance → search pill (search variant) → each trailing action
Enter / SpaceActivates the focused affordance; in the search pill, fires mdSearchActivate
Typing in the pillFires mdSearchInput per keystroke

The search pill shows a focus ring only under keyboard modality, so a pointer click does not leave a ring behind.

Tab through it — every control has its own accessible name Open in Storybook
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-app-bar headline="Company mail" subtitle="Accessible header" leading-icon="menu" leading-icon-label="Open navigation menu">
  <md-icon-button slot="trailing" icon="search" variant="standard" size="md" button-width="narrow" aria-label="Search mail"></md-icon-button>
  <md-icon-button slot="trailing" icon="more_vert" variant="standard" size="md" button-width="narrow" aria-label="More options"></md-icon-button>
  <md-avatar slot="trailing" name="Ada Lovelace" label="Account for Ada Lovelace"></md-avatar>
</md-app-bar>

RTL — leading/trailing sides, title alignment and padding mirror under dir="rtl"; every box metric is a logical property. See RTL.

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

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

<div style="display:grid;grid-template-columns:auto 1fr;gap:20px 16px;align-items:center;">
  <span style="inline-size:2.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">ltr</span>
  <div dir="ltr">
    <md-app-bar headline="Inbox" subtitle="12 unread" leading-icon="arrow_back" leading-icon-label="Back">
      <md-icon-button slot="trailing" icon="more_vert" variant="standard" size="md" button-width="narrow" aria-label="More options"></md-icon-button>
    </md-app-bar>
  </div>
  <span style="inline-size:2.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">rtl</span>
  <div dir="rtl">
    <md-app-bar headline="البريد الوارد" subtitle="12 غير مقروءة" leading-icon="arrow_back" leading-icon-label="رجوع">
      <md-icon-button slot="trailing" icon="more_vert" variant="standard" size="md" button-width="narrow" aria-label="خيارات أخرى"></md-icon-button>
    </md-app-bar>
  </div>
</div>

density="-1…-4" is a local override of the inherited data-density. It tapers the row height (64px → 48px floor), the icon touch targets, the glyph size, the avatar, the title type scale and the expanded heights together.

Density 0 through -4 — row height, icons and type all taper
0 -1 -2 -3 -4
Show code for each technology
<!-- index.html <head> — the icon font the components draw from -->
<link rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">

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

<div style="display:grid;grid-template-columns:auto 1fr;gap:16px;align-items:center;inline-size:100%;">
  <span style="inline-size:2rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">0</span>
  <md-app-bar density="0" headline="Density 0" leading-icon="menu" leading-icon-label="Open navigation"><md-icon-button slot="trailing" icon="more_vert" variant="standard" size="md" button-width="narrow" aria-label="More options"></md-icon-button></md-app-bar>
  <span style="inline-size:2rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">-1</span>
  <md-app-bar density="-1" headline="Density -1" leading-icon="menu" leading-icon-label="Open navigation"><md-icon-button slot="trailing" icon="more_vert" variant="standard" size="md" button-width="narrow" aria-label="More options"></md-icon-button></md-app-bar>
  <span style="inline-size:2rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">-2</span>
  <md-app-bar density="-2" headline="Density -2" leading-icon="menu" leading-icon-label="Open navigation"><md-icon-button slot="trailing" icon="more_vert" variant="standard" size="md" button-width="narrow" aria-label="More options"></md-icon-button></md-app-bar>
  <span style="inline-size:2rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">-3</span>
  <md-app-bar density="-3" headline="Density -3" leading-icon="menu" leading-icon-label="Open navigation"><md-icon-button slot="trailing" icon="more_vert" variant="standard" size="md" button-width="narrow" aria-label="More options"></md-icon-button></md-app-bar>
  <span style="inline-size:2rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">-4</span>
  <md-app-bar density="-4" headline="Density -4" leading-icon="menu" leading-icon-label="Open navigation"><md-icon-button slot="trailing" icon="more_vert" variant="standard" size="md" button-width="narrow" aria-label="More options"></md-icon-button></md-app-bar>
</div>

Translate headline, subtitle, leading-icon-label, search-placeholder, search-aria-label and every trailing control’s label. Long translated titles are exactly the case medium / large exists for — small never wraps, so a long string truncates there.

Localised headline, subtitle and affordance labels Open in Storybook
de ar
Show code for each technology
<!-- index.html <head> — the icon font the components draw from -->
<link rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">

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

<div style="display:grid;grid-template-columns:auto 1fr;gap:20px 16px;align-items:center;">
  <span style="inline-size:2rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">de</span>
  <md-app-bar variant="medium" headline="Posteingang" subtitle="12 ungelesene Nachrichten" leading-icon="menu" leading-icon-label="Navigation öffnen">
    <md-icon-button slot="trailing" icon="more_vert" variant="standard" size="md" button-width="narrow" aria-label="Weitere Optionen"></md-icon-button>
  </md-app-bar>
  <span style="inline-size:2rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">ar</span>
  <div dir="rtl">
    <md-app-bar variant="medium" headline="البريد الوارد" subtitle="12 رسالة غير مقروءة" leading-icon="menu" leading-icon-label="فتح التنقل">
      <md-icon-button slot="trailing" icon="more_vert" variant="standard" size="md" button-width="narrow" aria-label="خيارات أخرى"></md-icon-button>
    </md-app-bar>
  </div>
</div>
Custom propertyPurposeDefault
--md-app-bar-container-colorContainer background at restsurface
--md-app-bar-container-color-scrolledContainer background when scrolledsurface-container
--md-app-bar-container-shapeCorner radiusshape-corner-none — M3 wants straight corners
--md-app-bar-container-elevationContainer shadowelevation-0
--md-app-bar-headline-color / -subtitle-colorTitle texton-surface / on-surface-variant
--md-app-bar-leading-icon-color / -trailing-icon-colorIconson-surface / on-surface-variant
--md-app-bar-row-heightAction row height64px, density-derived
--md-app-bar-padding-inline-start / -endRow padding4px
--md-app-bar-icon-size / -icon-button-sizeGlyph / touch target24px / 48px, density-derived
--md-app-bar-trailing-gap / -trailing-touch-sizeTrailing cluster2px / 48px
--md-app-bar-avatar-sizeAvatar in trailing slots32px
--md-app-bar-medium-height / -medium-height-with-subtitleMedium expanded112px / 136px
--md-app-bar-large-height / -large-height-with-subtitleLarge expanded120px / 152px
--md-app-bar-search-container-color / -scrolledSearch pill fillsurface-container-highest / surface-container
--md-app-bar-search-container-height / -shapeSearch pill box56px, density-derived
--md-app-bar-search-padding-inline-start / -endHint inset / in-pill inset24px / 8px
--md-app-bar-search-placeholder-color / -input-colorHint / typed texton-surface-variant / on-surface
Themed instance Open in Storybook
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-app-bar
  headline="Themed"
  leading-icon="menu"
  leading-icon-label="Open navigation"
  style="--md-app-bar-container-color: var(--md-sys-color-primary-container); --md-app-bar-headline-color: var(--md-sys-color-on-primary-container); --md-app-bar-leading-icon-color: var(--md-sys-color-on-primary-container); --md-app-bar-trailing-icon-color: var(--md-sys-color-on-primary-container);">
  <md-icon-button slot="trailing" icon="more_vert" variant="standard" size="md" button-width="narrow" aria-label="More options"></md-icon-button>
</md-app-bar>

The two container colours are independent, so a bar can be flat at rest and tonal once scrolled — or branded in both states. Shape and elevation are yours too, though M3 wants a square, flat header.

Container colour at rest, when scrolled, plus shape and elevation
Show code for each technology
<!-- index.html <head> — the icon font the components draw from -->
<link rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">

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

<div style="display:flex;flex-direction:column;gap:24px;inline-size:100%;">
  <md-app-bar headline="Tonal, both states" leading-icon="menu" leading-icon-label="Open navigation"
    style="--md-app-bar-container-color: var(--md-sys-color-secondary-container); --md-app-bar-headline-color: var(--md-sys-color-on-secondary-container); --md-app-bar-leading-icon-color: var(--md-sys-color-on-secondary-container); --md-app-bar-trailing-icon-color: var(--md-sys-color-on-secondary-container);">
    <md-icon-button slot="trailing" icon="more_vert" variant="standard" size="md" button-width="narrow" aria-label="More options"></md-icon-button>
  </md-app-bar>

  <md-app-bar scrolled headline="Scrolled recolour" leading-icon="menu" leading-icon-label="Open navigation"
    style="--md-app-bar-container-color-scrolled: var(--md-sys-color-tertiary-container); --md-app-bar-headline-color: var(--md-sys-color-on-tertiary-container); --md-app-bar-leading-icon-color: var(--md-sys-color-on-tertiary-container); --md-app-bar-trailing-icon-color: var(--md-sys-color-on-tertiary-container);">
    <md-icon-button slot="trailing" icon="more_vert" variant="standard" size="md" button-width="narrow" aria-label="More options"></md-icon-button>
  </md-app-bar>

  <md-app-bar headline="Rounded and raised" leading-icon="menu" leading-icon-label="Open navigation"
    style="--md-app-bar-container-shape: 20px; --md-app-bar-container-elevation: var(--md-sys-elevation-level3); --md-app-bar-container-color: var(--md-sys-color-surface-container-high);">
    <md-icon-button slot="trailing" icon="more_vert" variant="standard" size="md" button-width="narrow" aria-label="More options"></md-icon-button>
  </md-app-bar>
</div>

Height, padding, glyph size, touch target and trailing gap are all separate properties, so a compact header does not have to go through density. Set them when you need one specific measurement rather than the whole taper.

Row height, padding, glyph size, touch target and trailing gap
Show code for each technology
<!-- index.html <head> — the icon font the components draw from -->
<link rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">

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

<div style="display:flex;flex-direction:column;gap:24px;inline-size:100%;">
  <md-app-bar headline="Default metrics" leading-icon="menu" leading-icon-label="Open navigation">
    <md-icon-button slot="trailing" icon="search" variant="standard" size="md" button-width="narrow" aria-label="Search"></md-icon-button>
    <md-icon-button slot="trailing" icon="more_vert" variant="standard" size="md" button-width="narrow" aria-label="More options"></md-icon-button>
  </md-app-bar>

  <md-app-bar headline="Taller row, wide padding, big glyphs" leading-icon="menu" leading-icon-label="Open navigation"
    style="--md-app-bar-row-height: 88px; --md-app-bar-padding-inline-start: 24px; --md-app-bar-padding-inline-end: 24px; --md-app-bar-icon-size: 32px; --md-app-bar-trailing-gap: 16px;">
    <md-icon-button slot="trailing" icon="search" variant="standard" size="md" button-width="narrow" aria-label="Search"></md-icon-button>
    <md-icon-button slot="trailing" icon="more_vert" variant="standard" size="md" button-width="narrow" aria-label="More options"></md-icon-button>
  </md-app-bar>

  <md-app-bar headline="Shorter row, tight glyphs" leading-icon="menu" leading-icon-label="Open navigation"
    style="--md-app-bar-row-height: 52px; --md-app-bar-icon-size: 20px; --md-app-bar-icon-button-size: 40px; --md-app-bar-trailing-gap: 0px;">
    <md-icon-button slot="trailing" icon="search" variant="standard" size="md" button-width="narrow" aria-label="Search"></md-icon-button>
    <md-icon-button slot="trailing" icon="more_vert" variant="standard" size="md" button-width="narrow" aria-label="More options"></md-icon-button>
  </md-app-bar>
</div>

medium and large each have two height properties, and the bar picks by whether a subtitle is present. Override the wrong one and nothing happens.

Expanded heights and the expanded block's inline padding
Show code for each technology
<!-- index.html <head> — the icon font the components draw from -->
<link rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">

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

<div style="display:flex;flex-direction:column;gap:24px;inline-size:100%;">
  <md-app-bar variant="medium" headline="Medium, custom no-subtitle height" leading-icon="arrow_back" leading-icon-label="Back"
    style="--md-app-bar-medium-height: 96px;"></md-app-bar>

    <md-app-bar variant="medium" headline="Medium with subtitle" subtitle="needs the -with-subtitle property" leading-icon="arrow_back" leading-icon-label="Back"
      style="--md-app-bar-medium-height-with-subtitle: 168px;"></md-app-bar>

      <md-app-bar variant="large" headline="Large, custom expanded padding" subtitle="wide inline inset" leading-icon="arrow_back" leading-icon-label="Back"
        style="--md-app-bar-large-height-with-subtitle: 172px; --md-app-bar-expanded-padding-inline: 40px;"></md-app-bar>
      </div>

The pill has its own colour, shape, height and inset properties, independent of the row around it.

Pill fill, shape, height and hint inset
Show code for each technology
<!-- index.html <head> — the icon font the components draw from -->
<link rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">

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

<div style="display:flex;flex-direction:column;gap:24px;inline-size:100%;">
  <md-app-bar variant="search" search-placeholder="Default pill" search-aria-label="Search" leading-icon="menu" leading-icon-label="Open navigation">
    <md-icon-button slot="search-trailing" icon="mic" variant="standard" size="md" button-width="narrow" aria-label="Voice search"></md-icon-button>
  </md-app-bar>

  <md-app-bar variant="search" search-placeholder="Squared, tinted, taller" search-aria-label="Search" leading-icon="menu" leading-icon-label="Open navigation"
    style="--md-app-bar-search-container-color: var(--md-sys-color-primary-container); --md-app-bar-search-container-shape: 8px; --md-app-bar-search-container-height: 64px; --md-app-bar-search-placeholder-color: var(--md-sys-color-on-primary-container); --md-app-bar-search-input-color: var(--md-sys-color-on-primary-container); --md-app-bar-search-trailing-icon-color: var(--md-sys-color-on-primary-container);">
    <md-icon-button slot="search-trailing" icon="mic" variant="standard" size="md" button-width="narrow" aria-label="Voice search"></md-icon-button>
  </md-app-bar>

  <md-app-bar variant="search" search-placeholder="Flush hint, wide row gap" search-aria-label="Search" leading-icon="menu" leading-icon-label="Open navigation"
    style="--md-app-bar-search-padding-inline-start: 8px; --md-app-bar-search-row-gap: 20px; --md-app-bar-search-container-color: var(--md-sys-color-surface-container-low);">
    <md-icon-button slot="search-trailing" icon="mic" variant="standard" size="md" button-width="narrow" aria-label="Voice search"></md-icon-button>
  </md-app-bar>
</div>

Every default resolves through an md-sys-color role, so a bar that sets no custom properties follows the theme on its own. Switch this page between light and dark — the bar below is untouched markup:

Untouched defaults — follows the page theme Open in Storybook
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-app-bar headline="Untouched defaults" subtitle="Follows the page theme" leading-icon="menu" leading-icon-label="Open navigation">
  <md-icon-button slot="trailing" icon="search" variant="standard" size="md" button-width="narrow" aria-label="Search"></md-icon-button>
  <md-avatar slot="trailing" name="Ada Lovelace" label="Account"></md-avatar>
</md-app-bar>

Custom properties cover colour and metrics. Parts reach into the shadow root for everything else — type treatment, layout of a wrapper, the pill’s internals.

PartElementExists when
rowThe action rowAlways
leadingLeading slot wrapperAlways
leading-iconThe prop-built leading md-icon-buttonOnly with leading-icon
icon / state-layerGlyph / state layer, exported from leading-iconOnly with leading-icon
titleInline titlesmall; hidden copy on medium / large
subtitleSubtitle textInline on small, expanded on flexible bars
expandedExpanded title blockmedium / large
expanded-headlineExpanded headline textmedium / large
trailingTrailing slot wrapperAlways
search-hostFlex bay holding the pillsearch
searchThe pill containersearch
search-state-layerHover / focus / press overlaysearch
search-fieldText region inside the pillsearch
search-inputThe built-in <input>search, no search slot
search-contentWrapper around slotted search contentsearch with a search slot
search-trailingIn-pill trailing baysearch
row, title, subtitle and trailing Open in Storybook
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>

<style>
  .parts-bar::part(title)    { text-transform: uppercase; letter-spacing: .1em; font-weight: 600; }
  .parts-bar::part(subtitle) { font-style: italic; opacity: .85; }
  .parts-bar::part(trailing) { gap: 12px; }
  .parts-bar::part(row)      { border-block-end: 2px solid var(--md-sys-color-primary); }
</style>
<md-app-bar class="parts-bar" headline="Styled parts" subtitle="title, subtitle, trailing, row" leading-icon="menu" leading-icon-label="Open navigation">
  <md-icon-button slot="trailing" icon="search" variant="standard" size="md" button-width="narrow" aria-label="Search"></md-icon-button>
  <md-icon-button slot="trailing" icon="more_vert" variant="standard" size="md" button-width="narrow" aria-label="More options"></md-icon-button>
</md-app-bar>

expanded is the wrapper and expanded-headline the text, so you can inset the block and restyle the headline independently.

expanded and expanded-headline on a large bar
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>

<style>
  .parts-flex::part(expanded) {
  background: var(--md-sys-color-surface-container-high);
  border-start-start-radius: 16px;
  border-start-end-radius: 16px;
  }
  .parts-flex::part(expanded-headline) {
  font-weight: 700;
  letter-spacing: -.01em;
  color: var(--md-sys-color-primary);
  }
  .parts-flex::part(subtitle) { text-transform: uppercase; letter-spacing: .12em; font-size: 11px; }
</style>
<md-app-bar class="parts-flex" variant="large" headline="Quarterly review" subtitle="Updated 2 minutes ago" leading-icon="arrow_back" leading-icon-label="Back">
  <md-icon-button slot="trailing" icon="more_vert" variant="standard" size="md" button-width="narrow" aria-label="More options"></md-icon-button>
</md-app-bar>
search, search-input and search-trailing
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>

<style>
  .parts-search::part(search) {
  border: 2px solid var(--md-sys-color-primary);
  border-radius: 12px;
  }
  .parts-search::part(search-input)  { font-weight: 600; letter-spacing: .02em; }
  .parts-search::part(search-trailing) { gap: 10px; }
  .parts-search::part(leading-icon)  { color: var(--md-sys-color-primary); }
</style>
<md-app-bar class="parts-search" variant="search" search-placeholder="Styled pill internals" search-aria-label="Search" leading-icon="menu" leading-icon-label="Open navigation">
  <md-icon-button slot="search-trailing" icon="mic" variant="standard" size="md" button-width="narrow" aria-label="Voice search"></md-icon-button>
  <md-avatar slot="trailing" name="Ada Lovelace" label="Account"></md-avatar>
</md-app-bar>
md-app-bar::part(title) {
letter-spacing: 0.15px;
}

md-toolbar · md-search · md-navigation-rail · md-navigation-bar · md-tabs · md-breadcrumbs · md-icon-button

For AI Agents — md-app-bar

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-app-bar 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-app-bar readme.md

# md-app-bar

<!-- llm:meta
tag: md-app-bar
category: navigation
status: md3-mapped
m3-guidelines: https://m3.material.io/components/app-bars/guidelines
form-associated: false
depends-on: md-icon-button, md-ripple
used-by: none
-->

**The screen's header.** Title, a leading navigation affordance, and trailing
actions — in small / medium / large heights, plus a dedicated **search**
variant for apps where search is the primary global function.

> Setup, theming, density and i18n are configured once for the whole library.
> Quick start: `import '@awc-ui/core/define';`

---

## When to use

- The **top of a screen**: current context, a back or menu affordance, and a
  small number of actions.
- A taller header for a prominent page title (`medium` / `large`).
- **Global search as the primary function** (`variant="search"`).

## When NOT to use

| Situation | Use instead |
|---|---|
| Local, page-specific action clusters | `md-toolbar` |
| Top-level destinations | `md-navigation-bar` / `md-navigation-rail` |
| Sibling views inside a screen | `md-tabs` |
| Search scoped to one view, with a results panel | `md-search` |
| Where-am-I hierarchy | `md-breadcrumbs` |
| A row of related actions | `md-button-group` |

## Decision cues

| Need | Setting |
|---|---|
| Standard header | `variant="small"` (default) |
| Prominent page title, may wrap to two lines | `variant="medium"` / `"large"` |
| Search is the app's primary global function | `variant="search"` |
| Centred title | `title-alignment="center"` |
| Tonal container while the page is scrolled | `scrolled` |
| Back or menu affordance | `leading-icon` + `leading-icon-label` |
| A second line under the title | `subtitle` |
| Your own search UI in the pill | `slot="search"` |

## API contract

```html
<md-app-bar
  variant="small|medium|large|search"   <!-- default: small -->
  title-alignment="start|center"        <!-- default: start -->
  headline="Inbox"
  subtitle="12 unread"
  leading-icon="menu"
  leading-icon-label="Open navigation"
  scrolled
  search-placeholder="Search"           <!-- default: "Search" -->
  search-value=""
  search-aria-label="Search mail"
  search-disabled
  density="-1|-2|-3|-4"                 <!-- default: 0 (uncompacted) -->
>
  <md-icon-button slot="trailing" icon="more_vert" aria-label="More"></md-icon-button>
</md-app-bar>
```

**Events** — all three bubble and are `composed`.

| Event | Detail | Fires |
|---|---|---|
| `mdLeadingClick` | `MouseEvent` | The **prop-based** `leading-icon` button is clicked |
| `mdSearchActivate` | `{ value }` | The built-in search field is focused, or `Enter`/`Space` is pressed in it |
| `mdSearchInput` | `{ value }` | Every keystroke in the built-in search field |

**Methods** — none.

**Slots** — `leading`, `headline`, `subtitle`, `trailing`, `search`,
`search-trailing`.

**Parts** — `row`, `leading`, `leading-icon`, `title`, `subtitle`, `expanded`,
`expanded-headline`, `trailing`, `search-host`, `search`, `search-state-layer`,
`search-field`, `search-input`, `search-content`, `search-trailing`. The
`leading-icon` button also exports `icon` and `state-layer`.

### Behavioral contract worth knowing

- The host is **`position: sticky; inset-block-start: 0`** with
  `z-index: var(--md-sys-z-index-app-bar, 100)` — it sticks to the top of its
  scroll container on its own. You do not need to position it.
- The host carries **`role="banner"`**. Only one banner landmark per page.
- **`scrolled` is a prop you set**, not something the bar detects. Wire it to
  your own scroll listener; it swaps the container (and the search pill) from
  `surface` to `surface-container`.
- **The `headline` / `subtitle` props win over the matching slots** — the slot
  is only rendered when the prop is empty. Setting both silently ignores the
  slot.
- The **`leading` slot wins over the `leading-icon` prop**: the built-in
  `md-icon-button` is rendered only when nothing is slotted. `mdLeadingClick`
  therefore fires only for the prop-based button — a slotted control needs its
  own click listener.
- `mdLeadingClick` reports the press and nothing else. Opening a drawer or
  navigating is yours.
- The **`trailing` slot is capped at 3 elements.** Extras get `hidden` set on
  them and a `console.warn` is logged.
- On `medium` / `large` the expanded headline block is always rendered and the
  inline row title is hidden (`opacity: 0`, `aria-hidden="true"`), so the
  headline is announced once.
- `subtitle` grows a flexible bar: medium 112 → 136px, large 120 → 152px.
- **Filling `slot="search"` replaces the built-in `<input>` entirely.** With it
  filled, `search-value`, `search-placeholder`, `search-aria-label` and both
  `mdSearchInput` / `mdSearchActivate` events are inert — your slotted field
  owns the interaction.
- `search-value` is mutable and reflected: typing writes it back to the
  attribute.
- `search-disabled` suppresses `mdSearchActivate`, drops the ripple, and dims
  the pill to 0.38 opacity.
- On `variant="search"`, `title-alignment="center"` centres only the
  **placeholder** (`::placeholder`); the caret and typed text stay
  inline-start-aligned so nothing shifts on focus.
- Below 600px (container **or** viewport width) the search pill tightens its
  hint inset and hides every `search-trailing` child after the first. Add
  `data-compact-keep` to a child to keep it.
- M3 gives `medium` / `large` a two-line headline allowance; `small` must not
  wrap.

---

## Do / Don't

Sourced from [M3 · App bars · Guidelines](https://m3.material.io/components/app-bars/guidelines).

| ✅ Do | ❌ Don't |
|---|---|
| Use a filled or tonal button for the one important action | Don't put multiple filled or tonal buttons in the app bar |
| Keep the search variant's trailing content sparse | Don't use three icons **and** an avatar in a search app bar |
| Use straight corners | Don't use curved shapes — it implies the container expands on interaction |
| Keep the default heights | Don't make an app bar shorter than its default height |
| Use `medium`/`large` and wrap to **two lines max** for long headlines | Don't wrap text in a `small` app bar |
| Use filled icons for clear, visible actions | Outlined icons only as needed, or for toggle buttons |
| Keep the leading affordance predictable (back or menu) | Don't overload the leading slot |

---

## Patterns

```html
<md-app-bar id="bar" headline="Inbox" leading-icon="menu"
            leading-icon-label="Open navigation">
  <md-icon-button slot="trailing" icon="search" aria-label="Search"></md-icon-button>
  <md-icon-button slot="trailing" icon="more_vert" aria-label="More options"></md-icon-button>
</md-app-bar>

<script type="module">
  const bar = document.getElementById('bar');
  bar.addEventListener('mdLeadingClick', () => openDrawer());

  // `scrolled` is yours to drive
  addEventListener('scroll', () => { bar.scrolled = window.scrollY > 0; }, { passive: true });
</script>
```

```html
<!-- Long headline: medium/large, two lines max -->
<md-app-bar variant="large" headline="Quarterly performance review"
            subtitle="Updated 5 minutes ago"></md-app-bar>
```

```html
<!-- Search as the app's primary function -->
<md-app-bar id="sbar" variant="search" search-placeholder="Search mail"
            search-aria-label="Search mail" leading-icon="menu"
            leading-icon-label="Open navigation">
  <md-icon-button slot="search-trailing" icon="mic" aria-label="Search by voice"></md-icon-button>
  <md-avatar slot="trailing" name="Ada Lovelace" initials="AL"></md-avatar>
</md-app-bar>

<script type="module">
  const sbar = document.getElementById('sbar');
  sbar.addEventListener('mdSearchInput',    (e) => suggest(e.detail.value));
  sbar.addEventListener('mdSearchActivate', (e) => openSearchView(e.detail.value));
</script>
```

```html
<!-- Slotted headline (only read when the props are empty) -->
<md-app-bar>
  <h1 slot="headline">Inbox</h1>
  <span slot="subtitle">12 unread</span>
</md-app-bar>
```

## Anti-patterns

| ❌ Wrong | ✅ Right | Why |
|---|---|---|
| Expecting `scrolled` to update itself | Set it from a scroll listener | It is a plain prop; the bar never reads scroll position. |
| Wrapping the bar in a `position: fixed` div to pin it | Leave it alone | The host is already `position: sticky; top: 0`. |
| `headline="Inbox"` **and** `<span slot="headline">` | Pick one | The prop wins; the slot is never rendered. |
| Expecting `mdLeadingClick` from a slotted leading button | Listen on the slotted control | The event belongs to the prop-based button only. |
| Four `md-icon-button`s in `slot="trailing"` | Keep three; move the rest into a menu | The 4th is hidden and warned about. |
| Using the search variant to get a results panel | `md-search` | This variant is a bar-integrated field, not a search surface. |
| Listening for `mdSearchInput` while using `slot="search"` | Listen on your own slotted field | The built-in input is not rendered, so the events never fire. |
| Two filled/tonal buttons in the bar | One emphasised action | M3 explicit rule. |
| Three icons plus an avatar in a search bar | Trim the trailing content | M3 explicit rule. |
| Rounded corners on the app bar | Straight corners | M3 explicit rule. |
| Reducing the bar below its default height | Keep the default | M3 explicit rule. |
| A wrapping headline in `small` | Use `medium`/`large` | M3 explicit rule. |
| A second `md-app-bar` on the page | One header per page | The host is `role="banner"`. |
| App bar used for local page actions | `md-toolbar` | Different purpose. |

## Accessibility, RTL, density, i18n

**Accessibility**
- The host is the page's `banner` landmark — render exactly one per page.
- Give the leading affordance a localized `leading-icon-label`; it is an
  icon-only control and would otherwise be nameless.
- Every trailing `md-icon-button` needs its own `aria-label`.
- The headline styling is not a heading. Slot a real heading element
  (`<h1 slot="headline">`) or provide one elsewhere in the document.
- On flexible bars the inline copy of the title is `aria-hidden`, so the
  headline is announced once, from the expanded block.
- For the search variant set `search-aria-label`; it falls back to
  `search-placeholder`, and a placeholder is not a reliable name.
- The built-in search field is `role="searchbox"` with `inputmode="search"`.
  Its focus ring is drawn only for keyboard focus.

**RTL** — leading/trailing sides, title alignment and padding are all logical
properties and mirror under `dir="rtl"`. A directional slotted leading glyph
(`arrow_back`) mirrors when you mark it `data-directional`.

**Density** — `density="-1…-4"` shortens the action row (64 → 48px), the icon
targets, the search pill and the expanded heights. Rung `0` is the uncompacted
default and is inert. M3 warns against going below the default height, so use
deep rungs sparingly here. To opt an app bar out of an inherited global
`data-density` rung, set `style="--md-sys-density-scale: 0"` on it.

**i18n** — translate `headline`, `subtitle`, `leading-icon-label`,
`search-placeholder`, `search-aria-label`, and the trailing controls' labels.
Long translated titles are exactly the case for `medium` / `large`.

## Related components

`md-toolbar` · `md-search` · `md-navigation-rail` · `md-navigation-bar` ·
`md-tabs` · `md-breadcrumbs` · `md-icon-button` · `md-avatar`

## Theming

| Custom property | Purpose | Default |
|---|---|---|
| `--md-app-bar-container-color` | Container background | `--md-sys-color-surface` |
| `--md-app-bar-container-color-scrolled` | Background while `scrolled` | `--md-sys-color-surface-container` |
| `--md-app-bar-container-shape` | Corner radius | `--md-sys-shape-corner-none` |
| `--md-app-bar-container-elevation` | Container shadow | `--md-sys-elevation-0` |
| `--md-app-bar-headline-color` | Title / expanded headline | `--md-sys-color-on-surface` |
| `--md-app-bar-subtitle-color` | Subtitle | `--md-sys-color-on-surface-variant` |
| `--md-app-bar-leading-icon-color` | Leading glyph | `--md-sys-color-on-surface` |
| `--md-app-bar-trailing-icon-color` | Trailing glyphs | `--md-sys-color-on-surface-variant` |
| `--md-app-bar-row-height` | Action row height | `64px` (tapers 4px/rung, floor 48px) |
| `--md-app-bar-padding-inline-start` / `-padding-inline-end` | Row gutters | `4px` |
| `--md-app-bar-row-padding-inline` | **Deprecated** alias, read only as the fallback for the two properties above. Do not use it in new code. | `4px` |
| `--md-app-bar-row-padding-block-start` | Row top padding | `0`; `20px` on `large` |
| `--md-app-bar-icon-gap` | Gap between icons and row content | `0px` |
| `--md-app-bar-icon-size` | Glyph size in the row | `24px` (tapers 1px/rung, floor 18px) |
| `--md-app-bar-icon-button-size` | Leading touch target | `48px` (tapers 4px/rung, floor 32px) |
| `--md-app-bar-trailing-icon-touch-size` | Trailing touch target | Same as the leading target |
| `--md-app-bar-trailing-gap` | Gap between trailing icons | `2px` |
| `--md-app-bar-avatar-size` | `md-avatar` size in `trailing` | `32px` (tapers 2px/rung, floor 24px) |
| `--md-app-bar-medium-height` / `-medium-height-with-subtitle` | Medium expanded height | `112px` / `136px` |
| `--md-app-bar-large-height` / `-large-height-with-subtitle` | Large expanded height | `120px` / `152px` |
| `--md-app-bar-expanded-padding-inline` | Expanded block gutters | `16px` |
| `--md-app-bar-expanded-padding-block-end` | Expanded block bottom padding | `24px`; `28px` on `large` |
| `--md-app-bar-search-container-color` | Search pill fill | `--md-sys-color-surface-container-highest` |
| `--md-app-bar-search-container-color-scrolled` | Search pill fill while `scrolled` | `--md-sys-color-surface-container` |
| `--md-app-bar-search-container-shape` | Search pill radius | `--md-sys-shape-corner-full` |
| `--md-app-bar-search-container-height` | Search pill height | `56px` (tapers 4px/rung, floor 40px) |
| `--md-app-bar-search-padding-inline-start` | Hint inset | `24px` (16px < 600px, 12px < 400px) |
| `--md-app-bar-search-padding-inline-end` | Pill end padding after in-pill icons | `8px` |
| `--md-app-bar-search-row-gap` | Gap between leading, pill and trailing | `8px` |
| `--md-app-bar-search-trailing-gap` | Gap between in-pill trailing icons | `4px` |
| `--md-app-bar-search-field-trailing-gap` | Gap between hint and in-pill icons | `6px` |
| `--md-app-bar-trailing-touch-size` | Outside trailing target on search bars | `48px` (tapers 4px/rung, floor 32px) |
| `--md-app-bar-search-placeholder-color` | Placeholder text | `--md-sys-color-on-surface-variant` |
| `--md-app-bar-search-input-color` | Typed search text | `--md-sys-color-on-surface` |
| `--md-app-bar-search-trailing-icon-color` | In-pill icons | `--md-sys-color-on-surface-variant` |

**CSS parts** — `row`, `leading`, `leading-icon`, `title`, `subtitle`,
`expanded`, `expanded-headline`, `trailing`, `search-host`, `search`,
`search-state-layer`, `search-field`, `search-input`, `search-content`,
`search-trailing`.

```css
md-app-bar.brand {
  --md-app-bar-container-color: var(--md-sys-color-primary-container);
  --md-app-bar-headline-color: var(--md-sys-color-on-primary-container);
}
```

<!-- Auto Generated Below -->


## Overview

`md-app-bar` — Material Design 3 App Bar.

Implements the current (M3 Expressive) app-bars specification:
  https://m3.material.io/components/app-bars/specs

Size variants: `small` (64px), `medium` (112/136px expanded), `large`
(120/152px expanded), and `search` (inline search field in the 64px row).
Flexible `medium` / `large` bars always render the expanded headline block.
Title alignment (`title-alignment`) is configuration, not a variant.

Selecting the search field should open an `md-search` view via `mdSearchActivate`.

## Properties

| Property            | Attribute            | Description                                                                                                                                                                                                                                                                                                   | Type                                         | Default    |
| ------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | ---------- |
| `density`           | `density`            | Density scale: 0 (default 64px row), -1 (60px), -2 (56px), -3 (52px), -4 (48px). Tightens the action row, icon targets, search field, and expanded heights.                                                                                                                                                   | `-1 \| -2 \| -3 \| -4 \| 0`                  | `0`        |
| `headline`          | `headline`           | Inline or expanded headline text. Falls back to the `headline` slot when empty.                                                                                                                                                                                                                               | `string`                                     | `''`       |
| `leadingIcon`       | `leading-icon`       | Material Symbols name for the leading navigation icon (slot takes priority).                                                                                                                                                                                                                                  | `string`                                     | `''`       |
| `leadingIconLabel`  | `leading-icon-label` | Accessible label for the prop-based leading icon button. Required when `leading-icon` is set.                                                                                                                                                                                                                 | `string`                                     | `''`       |
| `scrolled`          | `scrolled`           | When true, applies the scrolled container surface colour per M3 app bar common-colors (`surface` → `surface-container`). Wire from your scroll listener; the component does not observe scroll position itself.                                                                                               | `boolean`                                    | `false`    |
| `searchAriaLabel`   | `search-aria-label`  | Search variant only: accessible name for the inline search field. Per M3 labeling guidance, falls back to `search-placeholder` when unset.                                                                                                                                                                    | `string`                                     | `''`       |
| `searchDisabled`    | `search-disabled`    | Search variant only: disable the inline search field.                                                                                                                                                                                                                                                         | `boolean`                                    | `false`    |
| `searchPlaceholder` | `search-placeholder` | Search variant only: hinted search text shown in the inline search field. Falls back to `placeholder` on the optional `search` slot when empty.                                                                                                                                                               | `string`                                     | `'Search'` |
| `searchValue`       | `search-value`       | Search variant only: current search field value (two-way bindable).                                                                                                                                                                                                                                           | `string`                                     | `''`       |
| `subtitle`          | `subtitle`           | Optional subtitle. On flexible variants it sits below the expanded headline (and grows the container); on `small` it stacks under the inline title. Falls back to the `subtitle` slot when empty.                                                                                                             | `string`                                     | `''`       |
| `titleAlignment`    | `title-alignment`    | Title horizontal alignment — `start` (leading edge, default) or `center`. On search bars it centers the placeholder when empty and unfocused; once focused or when typing, the caret and text align to the inline-start edge.                                                                                 | `"center" \| "start"`                        | `'start'`  |
| `variant`           | `variant`            | App bar size variant.  - `small` — 64px action row with inline title-large headline. - `medium` — flexible 112px (136px with subtitle) expanded headline block. - `large` — flexible 120px (152px with subtitle) expanded headline block. - `search` — 64px row with inline search field (M3 search app bar). | `"large" \| "medium" \| "search" \| "small"` | `'small'`  |


## Events

| Event              | Description                                                                                                                                                                                  | Type                              |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- |
| `mdLeadingClick`   | Emitted when the prop-based leading icon button is activated.                                                                                                                                | `CustomEvent<MouseEvent>`         |
| `mdSearchActivate` | Search variant only: emitted when the inline search field is selected (click, focus, Enter, or Space). Wire this to `md-search.show()` to open the full-screen search view per the MD3 spec. | `CustomEvent<{ value: string; }>` |
| `mdSearchInput`    | Search variant only: emitted on every inline search field input change.                                                                                                                      | `CustomEvent<{ value: string; }>` |


## Shadow Parts

| Part                   | Description |
| ---------------------- | ----------- |
| `"expanded"`           |             |
| `"expanded-headline"`  |             |
| `"leading"`            |             |
| `"leading-icon"`       |             |
| `"row"`                |             |
| `"search"`             |             |
| `"search-content"`     |             |
| `"search-field"`       |             |
| `"search-host"`        |             |
| `"search-input"`       |             |
| `"search-state-layer"` |             |
| `"search-trailing"`    |             |
| `"subtitle"`           |             |
| `"title"`              |             |
| `"trailing"`           |             |


## Dependencies

### Depends on

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

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

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

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

AWC UI Operator’s Manual

# main-llm.md — AWC UI build director

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

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

Your job, in order:

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

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

---

## §1 — Interview the user

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

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

### 1.1 Scope and shape

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

### 1.2 Look and feel

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

### 1.3 Internationalization

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

### 1.4 Data and forms

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

### 1.5 Constraints

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

---

## §2 — Map answers to configuration

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

### 2.1 Global switches — the complete set

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

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

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

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

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

---

## §3 — Install and bootstrap

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

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

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

defineCustomElements(window);
```

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

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

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

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

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

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

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

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

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

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

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

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

---

## §4 — Global configuration reference

### 4.1 The token system

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

#### Color roles

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

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

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

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

#### Shape tokens

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

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

#### Elevation tokens

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

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

#### Motion tokens

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

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

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

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

#### State-layer opacities

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

#### Typography, spacing and layering

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

### 4.2 Theming and rebranding

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

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

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

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

### 4.3 Density

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

Two details that bite:

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

### 4.4 Dark mode

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

### 4.5 RTL

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

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

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

### 4.6 Internationalization

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

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

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

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

### 4.7 Forms and validation

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

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

Rules:

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

---

## §5 — Component decision matrix

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

### 5.1 Actions

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

### 5.2 Text input

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

### 5.3 Selection

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

### 5.4 Navigation

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

### 5.5 Containment and feedback

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

### 5.6 Data

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

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

### 5.7 Choosing the variant

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

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

### 5.8 Not in the library

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

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

---

## §6 — Component inventory

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

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

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

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

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

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

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

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

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

---

## §7 — Composition rules

### 7.1 What nests inside what

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

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

### 7.2 Pairs that belong together

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

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

### 7.3 Nesting that is always wrong

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

---

## §8 — Page recipes

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

### 8.1 Login screen

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

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

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

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

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

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

### 8.2 Settings page (mobile)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

### 8.4 Dashboard with a FAB

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

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

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

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

### 8.5 Destructive confirmation dialog

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

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

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

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

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

### 8.6 Tabbed content page

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

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

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

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

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

### 8.7 The full recipe library

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

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

---

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

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

### 9.1 API and styling

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

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

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

### 9.2 Content and hierarchy

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

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

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

### 9.3 Accessibility contract

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

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

---

## §10 — Before you ship

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