Skip to content

Breadcrumbs

Where the user is in a hierarchy, and the way back up. Renders a trail of md-breadcrumb-item children as a labelled navigation region with separators, and collapses the middle of a long trail behind an overflow control.

Live preview Open in Storybook
Home Library Data Tables
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-breadcrumbs label="Breadcrumb">
  <md-breadcrumb-item href="/" icon="home">Home</md-breadcrumb-item>
  <md-breadcrumb-item href="/library">Library</md-breadcrumb-item>
  <md-breadcrumb-item href="/library/data">Data</md-breadcrumb-item>
  <md-breadcrumb-item current>Tables</md-breadcrumb-item>
</md-breadcrumbs>

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


<md-breadcrumbs></md-breadcrumbs>
  • A hierarchical structure the user navigates into and back out of: file trees, category pages, nested settings.
  • The path itself carries meaning and users need to jump to an ancestor.
SituationUse instead
A flat site with 2–3 destinationsmd-navigation-bar / md-tabs
A linear processmd-stepper
Sibling views within a screenmd-tabs
Top-level navigationmd-navigation-rail / md-navigation-bar
Undo/back onlyA back md-icon-button in md-app-bar
A single level deepNothing — the page header says it
NeedSetting
Mark the page you are oncurrent on the last crumb
A different separator glyphseparator="›"
Fold a long trailmax-items > 0
Keep more crumbs at the endsitems-before-collapse / items-after-collapse
A leading glyph on a crumbicon prop, or the icon slot
Announce a level without linking itdisabled
Tighter rowsdensity="-1…-4"

You render the crumbs. The component does not read your router, your URL, or your route table — it lays out the children you give it, in DOM order, and that order is the hierarchy the user sees and hears.

Three crumbs in DOM order — the last one is the page you are on
Home Library Tables
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-breadcrumbs label="Breadcrumb">
  <md-breadcrumb-item href="/" icon="home">Home</md-breadcrumb-item>
  <md-breadcrumb-item href="/library">Library</md-breadcrumb-item>
  <md-breadcrumb-item current>Tables</md-breadcrumb-item>
</md-breadcrumbs>

Two rules that are easy to get wrong:

  • The last crumb should be current. It is the page you are on, so it renders as static text, not a link. The component auto-promotes the last child to current unless you have explicitly marked a different one, but marking it yourself is clearer — and never give a current crumb an href.
  • Breadcrumbs are hierarchy, not history. Order them by structure, never by what the user visited most recently. A trail built from browsing history is a different feature wearing the wrong clothes.

Breadcrumbs also supplement navigation. They tell the user where they are and let them jump up a level; they are not a substitute for a nav bar or rail. If the only way out of a page is the trail, the navigation is broken.

A crumb takes a leading glyph from the icon prop, or from its icon slot when you need custom artwork. The slot wins over the prop. An icon is decoration on top of the label — never the crumb’s only content, or the level loses its accessible name.

Icon prop, and a slotted SVG Open in Storybook
Home Library Data Tables Home Library Tables
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:20px;inline-size:100%;">
  <md-breadcrumbs label="Icon prop">
    <md-breadcrumb-item href="/" icon="home">Home</md-breadcrumb-item>
    <md-breadcrumb-item href="/library" icon="folder">Library</md-breadcrumb-item>
    <md-breadcrumb-item href="/library/data" icon="database">Data</md-breadcrumb-item>
    <md-breadcrumb-item current icon="table">Tables</md-breadcrumb-item>
  </md-breadcrumbs>

  <md-breadcrumbs label="Slotted icon">
    <md-breadcrumb-item href="/">
      <svg slot="icon" viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M12 3l9 8h-3v10h-5v-6h-2v6H6V11H3z"/></svg>
      Home
    </md-breadcrumb-item>
    <md-breadcrumb-item href="/library">Library</md-breadcrumb-item>
    <md-breadcrumb-item current>Tables</md-breadcrumb-item>
  </md-breadcrumbs>
</div>

The current crumb’s icon swaps to the filled (FILL=1) face, so “you are here” reads at a glance rather than only from the missing link styling.

disabled renders the level without an anchor and out of the tab order. The crumb is still announced, so the hierarchy stays intact — it simply cannot be navigated to. Use it for a level that exists structurally but has no page of its own, not to signal “loading”.

A disabled level — announced, not navigable, skipped by Tab Open in Storybook
Home Archive 2026 Q3
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-breadcrumbs label="Breadcrumb">
  <md-breadcrumb-item href="/" icon="home">Home</md-breadcrumb-item>
  <md-breadcrumb-item disabled>Archive</md-breadcrumb-item>
  <md-breadcrumb-item href="/archive/2026">2026</md-breadcrumb-item>
  <md-breadcrumb-item current>Q3</md-breadcrumb-item>
</md-breadcrumbs>

When the trail exceeds max-items, the middle crumbs hide behind a ”…” overflow button.

PropDefaultMeaning
max-items0Collapse only when the crumb count exceeds this. 0 disables collapsing
items-before-collapse1Crumbs always kept at the start
items-after-collapse2Crumbs always kept at the end
Collapsed trail — click the … to expand Open in Storybook
Home Projects Q3 Migration Services Billing Invoices
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-breadcrumbs max-items="4" items-before-collapse="1" items-after-collapse="2">
  <md-breadcrumb-item href="/" icon="home">Home</md-breadcrumb-item>
  <md-breadcrumb-item href="/a">Projects</md-breadcrumb-item>
  <md-breadcrumb-item href="/a/b">Q3 Migration</md-breadcrumb-item>
  <md-breadcrumb-item href="/a/b/c">Services</md-breadcrumb-item>
  <md-breadcrumb-item href="/a/b/c/d">Billing</md-breadcrumb-item>
  <md-breadcrumb-item current>Invoices</md-breadcrumb-item>
</md-breadcrumbs>

expand() and collapse() drive the same state programmatically. Both emit mdExpand whenever the boolean actually changes — including when collapsing is disabled, where only the visual result is a no-op. With max-items="0", expand() still fires mdExpand{ expanded: true } even though the trail was already fully visible, so guard on max-items in your own listener rather than assuming the event implies a layout change.

Collapsing is what makes a trail survive a narrow column. The same five crumbs, with progressively tighter max-items:

Uncollapsed, then max-items 4 and 3 Open in Storybook
Home Projects Q3 Migration Services Billing Home Projects Q3 Migration Services Billing Home Projects Q3 Migration Services Billing
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:20px;inline-size:100%;">
  <md-breadcrumbs label="No collapsing">
    <md-breadcrumb-item href="/" icon="home">Home</md-breadcrumb-item>
    <md-breadcrumb-item href="/a">Projects</md-breadcrumb-item>
    <md-breadcrumb-item href="/a/b">Q3 Migration</md-breadcrumb-item>
    <md-breadcrumb-item href="/a/b/c">Services</md-breadcrumb-item>
    <md-breadcrumb-item current>Billing</md-breadcrumb-item>
  </md-breadcrumbs>

  <md-breadcrumbs label="max-items 4" max-items="4">
    <md-breadcrumb-item href="/" icon="home">Home</md-breadcrumb-item>
    <md-breadcrumb-item href="/a">Projects</md-breadcrumb-item>
    <md-breadcrumb-item href="/a/b">Q3 Migration</md-breadcrumb-item>
    <md-breadcrumb-item href="/a/b/c">Services</md-breadcrumb-item>
    <md-breadcrumb-item current>Billing</md-breadcrumb-item>
  </md-breadcrumbs>

  <md-breadcrumbs label="max-items 3" max-items="3" items-before-collapse="1" items-after-collapse="1">
    <md-breadcrumb-item href="/" icon="home">Home</md-breadcrumb-item>
    <md-breadcrumb-item href="/a">Projects</md-breadcrumb-item>
    <md-breadcrumb-item href="/a/b">Q3 Migration</md-breadcrumb-item>
    <md-breadcrumb-item href="/a/b/c">Services</md-breadcrumb-item>
    <md-breadcrumb-item current>Billing</md-breadcrumb-item>
  </md-breadcrumbs>
</div>

separator takes a short string — / (default), , , ». The parent owns it: every crumb reads the glyph from the parent and renders its own leading separator, so the trail stays visually uniform.

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

<md-breadcrumbs separator="›">
  <md-breadcrumb-item href="/">Home</md-breadcrumb-item>
  <md-breadcrumb-item href="/library">Library</md-breadcrumb-item>
  <md-breadcrumb-item current>Tables</md-breadcrumb-item>
</md-breadcrumbs>
/ › → » •
HomeLibraryTables HomeLibraryTables HomeLibraryTables HomeLibraryTables HomeLibraryTables
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<div style="display:flex;flex-direction:column;gap:20px;inline-size:100%;">
  <md-breadcrumbs separator="/"><md-breadcrumb-item href="/">Home</md-breadcrumb-item><md-breadcrumb-item href="/library">Library</md-breadcrumb-item><md-breadcrumb-item current>Tables</md-breadcrumb-item></md-breadcrumbs>
  <md-breadcrumbs separator="›"><md-breadcrumb-item href="/">Home</md-breadcrumb-item><md-breadcrumb-item href="/library">Library</md-breadcrumb-item><md-breadcrumb-item current>Tables</md-breadcrumb-item></md-breadcrumbs>
  <md-breadcrumbs separator="→"><md-breadcrumb-item href="/">Home</md-breadcrumb-item><md-breadcrumb-item href="/library">Library</md-breadcrumb-item><md-breadcrumb-item current>Tables</md-breadcrumb-item></md-breadcrumbs>
  <md-breadcrumbs separator="»"><md-breadcrumb-item href="/">Home</md-breadcrumb-item><md-breadcrumb-item href="/library">Library</md-breadcrumb-item><md-breadcrumb-item current>Tables</md-breadcrumb-item></md-breadcrumbs>
  <md-breadcrumbs separator="•"><md-breadcrumb-item href="/">Home</md-breadcrumb-item><md-breadcrumb-item href="/library">Library</md-breadcrumb-item><md-breadcrumb-item current>Tables</md-breadcrumb-item></md-breadcrumbs>
</div>
Separator replaced with an inline SVG via ::part Open in Storybook
Home Library Data Tables
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>
  /* md-breadcrumbs exposes a separator part too (the one before the …
  toggle), so a full restyle needs both selectors. */
  .svg-sep::part(separator),
  .svg-sep md-breadcrumb-item::part(separator) {
  color: transparent;
  inline-size: 16px;
  background: center / 12px no-repeat
  url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23888'><path d='M9 6l6 6-6 6z'/></svg>");
  }
</style>
<md-breadcrumbs class="svg-sep" label="Breadcrumb">
  <md-breadcrumb-item href="/" icon="home">Home</md-breadcrumb-item>
  <md-breadcrumb-item href="/library">Library</md-breadcrumb-item>
  <md-breadcrumb-item href="/library/data">Data</md-breadcrumb-item>
  <md-breadcrumb-item current>Tables</md-breadcrumb-item>
</md-breadcrumbs>

Separators are decorative and are marked aria-hidden — they are never announced, so replacing the glyph never changes what a screen reader says.

EventOnCancelableDetailFires
mdSelectmd-breadcrumb-itemyesMdBreadcrumbSelectDetailA linked crumb is activated — click, or Enter on the focused anchor
mdExpandmd-breadcrumbsnoMdBreadcrumbsExpandDetailThe overflow toggle is activated, or expand() / collapse() changes the state

mdSelect is emitted by the child, but it bubbles and is composed — attach one delegated listener on the md-breadcrumbs element. Only the anchor emits it, so current and disabled crumbs stay silent. Being cancelable is the hook for SPA routing: keep real hrefs so right-click, middle-click and “copy link” still work, then intercept.

You want to…Listen to
Route in an SPA instead of a full page loadmdSelect + preventDefault()
Log which ancestor the user jumped tomdSelectdetail.href
Know the trail folded or unfoldedmdExpanddetail.expanded
Re-measure layout after the trail changes widthmdExpanddetail.itemCount
interface MdBreadcrumbsExpandDetail {
expanded: boolean; // the state the trail has JUST taken
itemCount: number;
}
interface MdBreadcrumbSelectDetail {
href: string;
label: string;
current: boolean;
itemIndex: number; // 0-indexed position in the trail
itemTotal: number;
originalEvent: MouseEvent | KeyboardEvent;
}

Activate the ”…” toggle to see mdExpand, then click a crumb to see mdSelect. Navigation is vetoed in this demo so the page stays put — ⌘/Ctrl-click still opens a new tab, because the listener bails out before preventDefault().

One delegated listener catches every crumb, plus the overflow toggle
Home Projects Q3 Migration Services Invoices Activate the … toggle, then click a crumb.
Show code for each technology
<!-- 5 crumbs against max-items="4" — the trail folds, so the … toggle
   (and therefore mdExpand) is actually reachable. -->
<md-breadcrumbs id="trail" label="Breadcrumb" max-items="4">
<md-breadcrumb-item href="/" icon="home">Home</md-breadcrumb-item>
<md-breadcrumb-item href="/a">Projects</md-breadcrumb-item>
<md-breadcrumb-item href="/a/b">Q3 Migration</md-breadcrumb-item>
<md-breadcrumb-item href="/a/b/c">Services</md-breadcrumb-item>
<md-breadcrumb-item current>Invoices</md-breadcrumb-item>
</md-breadcrumbs>

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

// mdSelect is emitted by the crumb but bubbles, so one listener on the
// trail covers all of them.
trail.addEventListener('mdSelect', (e) => {
  const { href, label, itemIndex, itemTotal } = e.detail;
  console.log(label, href, itemIndex + 1, 'of', itemTotal);
});

trail.addEventListener('mdExpand', (e) => {
  console.log(e.detail.expanded, e.detail.itemCount);
});
</script>

Reading the event is one thing; taking over navigation is another. Keep the real href so the browser affordances survive, then cancel mdSelect and hand the URL to your router:

<md-breadcrumbs id="trail" label="Breadcrumb">
<md-breadcrumb-item href="/" icon="home">Home</md-breadcrumb-item>
<md-breadcrumb-item href="/library">Library</md-breadcrumb-item>
<md-breadcrumb-item current>Tables</md-breadcrumb-item>
</md-breadcrumbs>

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

trail.addEventListener('mdSelect', (e) => {
  const { href, originalEvent } = e.detail;
  if (originalEvent.metaKey || originalEvent.ctrlKey) return;  // let new-tab work
  e.preventDefault();
  router.push(href);
});
</script>

Properties

PropertyAttributeTypeDefaultReflects
labellabelstring'Breadcrumb'
expandLabelexpand-labelstring'Show full breadcrumb trail'
separatorseparatorstring'/'
maxItemsmax-itemsnumber0
itemsBeforeCollapseitems-before-collapsenumber1
itemsAfterCollapseitems-after-collapsenumber2
densitydensity0 | -1 | -2 | -3 | -40Yes

Methods

MethodParameters
expand()none
collapse()none

Slots

SlotDescription
(default)

CSS Custom Properties

Override on the host element for per-instance theming:

PropertyDescription
--md-breadcrumbs-gapGap between items / separators
--md-breadcrumbs-separator-colorSeparator glyph colour
--md-breadcrumbs-text-colorItem label colour (resting)
--md-breadcrumbs-staggerPer-item entrance delay

CSS Shadow Parts

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

PartDescription
listThe <ol> element.
overflowThe "…" collapse list item.
overflow-buttonThe button inside the overflow item.

A trail is built from one sub-component. It is documented here rather than on its own page: it means nothing outside md-breadcrumbs, and the parent writes several of its attributes for you.

One crumb in the trail — a link to an ancestor, or the non-interactive current page.

  • collapsed, item-index and item-total are parent-managed. md-breadcrumbs writes them on every render to drive the collapse window and each crumb’s leading separator. Setting them yourself breaks the collapse logic.
  • current and href don’t go together. current renders the crumb as static text (the static part) instead of an anchor — the correct treatment for the page you are on, which should not link to itself. The parent auto-promotes the last child to current unless you explicitly marked another one.
  • The separator is drawn per-crumb but owned by the parent. Each item reads the glyph from the parent’s separator prop and renders it before itself when item-index > 0. The item’s separator part exists for styling reach, not for you to supply a separator.
  • With an href the crumb is a real anchor, so middle-click, right-click and “copy link address” work. mdSelect fires on activation — including Enter on the focused anchor, which the browser synthesises as a click.
  • disabled renders the crumb without an anchor and out of the tab order: the level stays announced but is not navigable.

Properties

PropertyAttributeTypeDefaultReflects
hrefhrefstring''
targettargetstring''
relrelstring''
currentcurrentbooleanfalseYes
iconiconstring''
disableddisabledbooleanfalseYes
collapsedcollapsedbooleanfalseYes
itemIndexitem-indexnumber0
itemTotalitem-totalnumber0
densitydensity0 | -1 | -2 | -3 | -40Yes

Slots

SlotDescription
(default)
icon

CSS Custom Properties

Override on the host element for per-instance theming:

PropertyDescription
--md-breadcrumb-item-link-colorLink text colour
--md-breadcrumb-item-current-colorCurrent-page text colour
--md-breadcrumb-item-current-bgCurrent-page pill background
--md-breadcrumb-item-disabled-colorDisabled text colour
--md-breadcrumb-item-hover-colorHover-state text colour
--md-breadcrumb-item-state-layer-colorHover/focus/press overlay colour
--md-breadcrumb-item-gapGap between icon + label
--md-breadcrumb-item-paddingInteractive padding

CSS Shadow Parts

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

PartDescription
separatorGlyph rendered before this item (when not first).
iconLeading icon container.
labelLabel text wrapper.
staticThe span (when current or no href).
linkThe anchor (when an href is set and not current).
state-layerHover/focus/press overlay.
  • The host is the navigation region: role="navigation" named by label (default "Breadcrumb"). Translate it.
  • The overflow toggle’s accessible name is expand-label (default "Show full breadcrumb trail"), and it carries aria-expanded. Translate it too — the English defaults are placeholders, not a design decision.
  • The current page should be marked current, which exposes “you are here” instead of yet another link.
  • Separators are decorative and aria-hidden; they are never part of the accessible name.
  • Keep DOM order identical to visual order — the trail is read in DOM order.
KeyAction
TabEach visible linked crumb, then the overflow toggle. current, disabled and collapsed crumbs are skipped
EnterActivates the focused crumb — the browser synthesises a click, so mdSelect fires
Enter / SpaceOn the overflow toggle, expands the trail and fires mdExpand

Tab through the trail below. It is collapsed (max-items="4" against six crumbs, with items-after-collapse="3" keeping the tail visible), so Docs and Components are display:none until you expand — a collapsed crumb is not in the tab order at all, because it is not rendered. Of what remains, focus lands on Home, then Breadcrumbs, then the ”…” toggle (the toggle is last in the DOM even though CSS order paints it in the middle), and never on Navigation (disabled) or Accessibility (current). Those two are still announced; they simply are not link targets. Activate the ”…” first if you want to Tab the whole path.

Tab through it — collapsed crumbs are unreachable; the disabled and current crumbs are skipped Open in Storybook
Home Docs Components Navigation Breadcrumbs Accessibility
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-breadcrumbs label="Documentation breadcrumb" expand-label="Show full breadcrumb trail" max-items="4" items-after-collapse="3">
  <md-breadcrumb-item href="/" icon="home">Home</md-breadcrumb-item>
  <md-breadcrumb-item href="/docs">Docs</md-breadcrumb-item>
  <md-breadcrumb-item href="/docs/components">Components</md-breadcrumb-item>
  <md-breadcrumb-item disabled>Navigation</md-breadcrumb-item>
  <md-breadcrumb-item href="/docs/components/breadcrumbs">Breadcrumbs</md-breadcrumb-item>
  <md-breadcrumb-item current>Accessibility</md-breadcrumb-item>
</md-breadcrumbs>

RTL — the trail direction mirrors under dir="rtl"; every box metric is a logical property. See RTL.

Same markup, dir=ltr vs dir=rtl Open in Storybook
ltr
Home Library Tables
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-breadcrumbs label="Breadcrumb">
      <md-breadcrumb-item href="/" icon="home">Home</md-breadcrumb-item>
      <md-breadcrumb-item href="/library">Library</md-breadcrumb-item>
      <md-breadcrumb-item current>Tables</md-breadcrumb-item>
    </md-breadcrumbs>
  </div>
  <span style="inline-size:2.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">rtl</span>
  <div dir="rtl">
    <md-breadcrumbs label="مسار التنقل">
      <md-breadcrumb-item href="/" icon="home">الرئيسية</md-breadcrumb-item>
      <md-breadcrumb-item href="/library">المكتبة</md-breadcrumb-item>
      <md-breadcrumb-item current>الجداول</md-breadcrumb-item>
    </md-breadcrumbs>
  </div>
</div>

/ and point nowhere, so RTL needs nothing from you. , and » all point right — under dir="rtl" they aim back the way the trail came. Swap the prop with the direction (, ), or pick a neutral glyph. Both rows below are dir="rtl"; only the second one had its glyph swapped.

Both rows are dir=rtl — only the second swapped › for ‹
wrong الرئيسية المكتبة الجداول right الرئيسية المكتبة الجداول
Show code for each technology
<!-- index.html <head> — the icon font the components draw from -->
<link rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">

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

<div dir="rtl" style="display:grid;grid-template-columns:auto 1fr;gap:14px 16px;align-items:center;">
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;direction:ltr;">wrong</span>
  <md-breadcrumbs label="مسار التنقل" separator="›">
    <md-breadcrumb-item href="/" icon="home">الرئيسية</md-breadcrumb-item>
    <md-breadcrumb-item href="/library">المكتبة</md-breadcrumb-item>
    <md-breadcrumb-item current>الجداول</md-breadcrumb-item>
  </md-breadcrumbs>
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;direction:ltr;">right</span>
  <md-breadcrumbs label="مسار التنقل" separator="‹">
    <md-breadcrumb-item href="/" icon="home">الرئيسية</md-breadcrumb-item>
    <md-breadcrumb-item href="/library">المكتبة</md-breadcrumb-item>
    <md-breadcrumb-item current>الجداول</md-breadcrumb-item>
  </md-breadcrumbs>
</div>

density="-1…-4" is a local override of the inherited data-density. Set it on the trail and it cascades to every crumb, tightening the inter-crumb gap, the icon-to-label gap and the crumb padding together. There is no 0 rung to write — omitting the prop is the default, and writing density="0" maps nothing at all.

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

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

<div style="display:grid;grid-template-columns:auto 1fr;gap:14px 16px;align-items:center;">
  <span style="inline-size:2rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">none</span>
  <md-breadcrumbs label="Default density"><md-breadcrumb-item href="/" icon="home">Home</md-breadcrumb-item><md-breadcrumb-item href="/library">Library</md-breadcrumb-item><md-breadcrumb-item current>Tables</md-breadcrumb-item></md-breadcrumbs>
  <span style="inline-size:2rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">-1</span>
  <md-breadcrumbs density="-1" label="Density -1"><md-breadcrumb-item href="/" icon="home">Home</md-breadcrumb-item><md-breadcrumb-item href="/library">Library</md-breadcrumb-item><md-breadcrumb-item current>Tables</md-breadcrumb-item></md-breadcrumbs>
  <span style="inline-size:2rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">-2</span>
  <md-breadcrumbs density="-2" label="Density -2"><md-breadcrumb-item href="/" icon="home">Home</md-breadcrumb-item><md-breadcrumb-item href="/library">Library</md-breadcrumb-item><md-breadcrumb-item current>Tables</md-breadcrumb-item></md-breadcrumbs>
  <span style="inline-size:2rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">-3</span>
  <md-breadcrumbs density="-3" label="Density -3"><md-breadcrumb-item href="/" icon="home">Home</md-breadcrumb-item><md-breadcrumb-item href="/library">Library</md-breadcrumb-item><md-breadcrumb-item current>Tables</md-breadcrumb-item></md-breadcrumbs>
  <span style="inline-size:2rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">-4</span>
  <md-breadcrumbs density="-4" label="Density -4"><md-breadcrumb-item href="/" icon="home">Home</md-breadcrumb-item><md-breadcrumb-item href="/library">Library</md-breadcrumb-item><md-breadcrumb-item current>Tables</md-breadcrumb-item></md-breadcrumbs>
</div>

The two are independent signals: dir comes from the document tree, density from the nearest data-density ancestor — and a local density="-1…-4" on a trail overrides the inherited rung without touching direction. All three trails below sit in one dir="rtl" data-density="-2" wrapper: the first takes the inherited -2, the second drops to -4 with the prop, and the third resets to the default rung the only way that works — setting --md-sys-density-scale: 0 on the element itself. A density="0" prop would have changed nothing.

dir=rtl with data-density=-2: inherited -2, a prop override to -4, and a reset to 0 via --md-sys-density-scale
-2 inherited الرئيسية المكتبة الجداول -4 local الرئيسية المكتبة الجداول 0 via var الرئيسية المكتبة الجداول
Show code for each technology
<!-- index.html <head> — the icon font the components draw from -->
<link rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">

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

<div dir="rtl" data-density="-2" style="display:grid;grid-template-columns:auto 1fr;gap:14px 16px;align-items:center;">
  <span style="inline-size:5.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;direction:ltr;">-2 inherited</span>
  <md-breadcrumbs label="مسار التنقل" separator="‹">
    <md-breadcrumb-item href="/" icon="home">الرئيسية</md-breadcrumb-item>
    <md-breadcrumb-item href="/library">المكتبة</md-breadcrumb-item>
    <md-breadcrumb-item current>الجداول</md-breadcrumb-item>
  </md-breadcrumbs>
  <span style="inline-size:5.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;direction:ltr;">-4 local</span>
  <md-breadcrumbs label="مسار التنقل" separator="‹" density="-4">
    <md-breadcrumb-item href="/" icon="home">الرئيسية</md-breadcrumb-item>
    <md-breadcrumb-item href="/library">المكتبة</md-breadcrumb-item>
    <md-breadcrumb-item current>الجداول</md-breadcrumb-item>
  </md-breadcrumbs>
  <span style="inline-size:5.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;direction:ltr;">0 via var</span>
  <md-breadcrumbs label="مسار التنقل" separator="‹" style="--md-sys-density-scale:0;">
    <md-breadcrumb-item href="/" icon="home">الرئيسية</md-breadcrumb-item>
    <md-breadcrumb-item href="/library">المكتبة</md-breadcrumb-item>
    <md-breadcrumb-item current>الجداول</md-breadcrumb-item>
  </md-breadcrumbs>
</div>

Densitydensity="-1…-4" on the trail cascades to every crumb; the inter-crumb gap, the icon-to-label gap and the crumb padding all taper together. See Density.

i18n — translate label, expand-label and every crumb’s text; the English defaults ("Breadcrumb", "Show full breadcrumb trail") are placeholders, not a design decision. Long translated crumbs are exactly when max-items earns its keep.

Localised region name, overflow label and crumbs Open in Storybook
de Startseite Verwaltungsübersicht Rechnungsstellung Belege 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-breadcrumbs label="Navigationspfad" expand-label="Vollständigen Navigationspfad anzeigen" max-items="3">
    <md-breadcrumb-item href="/" icon="home">Startseite</md-breadcrumb-item>
    <md-breadcrumb-item href="/a">Verwaltungsübersicht</md-breadcrumb-item>
    <md-breadcrumb-item href="/a/b">Rechnungsstellung</md-breadcrumb-item>
    <md-breadcrumb-item current>Belege</md-breadcrumb-item>
  </md-breadcrumbs>
  <span style="inline-size:2rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">ar</span>
  <div dir="rtl">
    <md-breadcrumbs label="مسار التنقل" expand-label="عرض مسار التنقل الكامل" separator="‹" max-items="3">
      <md-breadcrumb-item href="/" icon="home">الرئيسية</md-breadcrumb-item>
      <md-breadcrumb-item href="/a">لوحة الإدارة</md-breadcrumb-item>
      <md-breadcrumb-item href="/a/b">الفوترة</md-breadcrumb-item>
      <md-breadcrumb-item current>الإيصالات</md-breadcrumb-item>
    </md-breadcrumbs>
  </div>
</div>

Two layers: the trail owns spacing and the separator, each crumb owns its own colours. Set the crumb properties on the parent and they cascade.

Custom propertyPurposeDefault
--md-breadcrumbs-gapSpace between crumbs and separators8px, density-derived
--md-breadcrumbs-separator-colorSeparator glyph colouron-surface-variant
--md-breadcrumbs-text-colorTrail text colour at reston-surface-variant
--md-breadcrumbs-staggerPer-crumb delay of the expand animation40ms
Custom propertyPurposeDefault
--md-breadcrumb-item-link-colorLink texton-surface-variant
--md-breadcrumb-item-hover-colorHover textprimary
--md-breadcrumb-item-current-colorCurrent-page texton-secondary-container
--md-breadcrumb-item-current-bgCurrent-page pillsecondary-container
--md-breadcrumb-item-disabled-colorDisabled text61% on-surface
--md-breadcrumb-item-state-layer-colorHover / focus / press overlayon-surface
--md-breadcrumb-item-gapIcon-to-label gap6px, density-derived
--md-breadcrumb-item-paddingCrumb paddingDensity-derived
Branded, tonal, and roomy — all set on the parent Open in Storybook
Home Library Tables Home Library Tables Home Library Tables
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:20px;inline-size:100%;">
  <md-breadcrumbs label="Branded" separator="›"
    style="--md-breadcrumbs-gap: 14px; --md-breadcrumbs-separator-color: #7c4dff; --md-breadcrumb-item-link-color: #7c4dff; --md-breadcrumb-item-hover-color: #4527a0; --md-breadcrumb-item-current-bg: #ede7f6; --md-breadcrumb-item-current-color: #311b92;">
    <md-breadcrumb-item href="/" icon="home">Home</md-breadcrumb-item>
    <md-breadcrumb-item href="/library">Library</md-breadcrumb-item>
    <md-breadcrumb-item current>Tables</md-breadcrumb-item>
  </md-breadcrumbs>

  <md-breadcrumbs label="Tonal roles"
    style="--md-breadcrumb-item-link-color: var(--md-sys-color-tertiary); --md-breadcrumb-item-hover-color: var(--md-sys-color-on-tertiary-container); --md-breadcrumb-item-current-bg: var(--md-sys-color-tertiary-container); --md-breadcrumb-item-current-color: var(--md-sys-color-on-tertiary-container); --md-breadcrumbs-separator-color: var(--md-sys-color-outline);">
    <md-breadcrumb-item href="/" icon="home">Home</md-breadcrumb-item>
    <md-breadcrumb-item href="/library">Library</md-breadcrumb-item>
    <md-breadcrumb-item current>Tables</md-breadcrumb-item>
  </md-breadcrumbs>

  <md-breadcrumbs label="Roomy" separator="→"
    style="--md-breadcrumbs-gap: 20px; --md-breadcrumb-item-padding: 10px 18px; --md-breadcrumb-item-gap: 12px;">
    <md-breadcrumb-item href="/" icon="home">Home</md-breadcrumb-item>
    <md-breadcrumb-item href="/library" icon="folder">Library</md-breadcrumb-item>
    <md-breadcrumb-item current icon="table">Tables</md-breadcrumb-item>
  </md-breadcrumbs>
</div>

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

Untouched defaults — follows the page theme Open in Storybook
Home Projects Q3 Migration Services Billing
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-breadcrumbs label="Breadcrumb" max-items="4">
  <md-breadcrumb-item href="/" icon="home">Home</md-breadcrumb-item>
  <md-breadcrumb-item href="/a">Projects</md-breadcrumb-item>
  <md-breadcrumb-item href="/a/b">Q3 Migration</md-breadcrumb-item>
  <md-breadcrumb-item href="/a/b/c">Services</md-breadcrumb-item>
  <md-breadcrumb-item current>Billing</md-breadcrumb-item>
</md-breadcrumbs>

Custom properties cover colour and spacing. Parts reach into the shadow root for everything else — and the two components expose separate sets.

PartOnElement
listmd-breadcrumbsThe <ol>
overflowmd-breadcrumbsThe ”…” list item
overflow-buttonmd-breadcrumbsThe button inside it
separatormd-breadcrumbsThe glyph the trail draws before the ”…” toggle (also tagged separator--2)
separatormd-breadcrumb-itemThe glyph before this crumb (absent on the first)
linkmd-breadcrumb-itemThe anchor — only with an href and not current
staticmd-breadcrumb-itemThe span — when current, disabled, or no href
state-layermd-breadcrumb-itemHover / focus / press overlay
iconmd-breadcrumb-itemLeading icon container
labelmd-breadcrumb-itemLabel text wrapper
list, overflow-button, label and separator
Home Projects Q3 Migration Services Billing
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-trail::part(list) { padding-block: 6px; }
  .parts-trail::part(overflow-button) {
  border-radius: 4px;
  outline: 1px solid var(--md-sys-color-primary);
  }
  .parts-trail md-breadcrumb-item::part(label) { text-transform: uppercase; letter-spacing: .08em; font-size: 12px; }
  /* Both selectors: the crumbs draw their own separators, the trail draws the
  one in front of the … toggle. */
  .parts-trail::part(separator),
  .parts-trail md-breadcrumb-item::part(separator) { opacity: .45; }
</style>
<md-breadcrumbs class="parts-trail" label="Breadcrumb" max-items="4">
  <md-breadcrumb-item href="/" icon="home">Home</md-breadcrumb-item>
  <md-breadcrumb-item href="/a">Projects</md-breadcrumb-item>
  <md-breadcrumb-item href="/a/b">Q3 Migration</md-breadcrumb-item>
  <md-breadcrumb-item href="/a/b/c">Services</md-breadcrumb-item>
  <md-breadcrumb-item current>Billing</md-breadcrumb-item>
</md-breadcrumbs>
link vs static — the current crumb is not an anchor
Home Library Tables
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-current md-breadcrumb-item::part(static) {
  border: 1px dashed var(--md-sys-color-outline);
  border-radius: 999px;
  }
  /* Underline the LABEL, not the link: ::part(link) is a flex container, so the
  browser draws the decoration once per flex item — the 18px glyph box and the
  20px text box each get their own, at their own baseline, which reads as two
  underlines at different heights. */
  .parts-current md-breadcrumb-item:not([current])::part(label) { text-decoration: underline; text-underline-offset: 3px; }
  .parts-current md-breadcrumb-item::part(icon) { color: var(--md-sys-color-primary); }
</style>
<md-breadcrumbs class="parts-current" label="Breadcrumb">
  <md-breadcrumb-item href="/" icon="home">Home</md-breadcrumb-item>
  <md-breadcrumb-item href="/library" icon="folder">Library</md-breadcrumb-item>
  <md-breadcrumb-item current icon="table">Tables</md-breadcrumb-item>
</md-breadcrumbs>
md-breadcrumbs::part(overflow-button) {
border-radius: 4px;
}

md-breadcrumb-item · md-app-bar · md-tabs · md-navigation-rail · md-menu

For AI Agents — md-breadcrumbs

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

# md-breadcrumbs

<!-- llm:meta
tag: md-breadcrumbs
category: navigation
status: custom
m3-guidelines: none — M3 has no breadcrumbs page
form-associated: false
depends-on: none
used-by: none
accepts-children: md-breadcrumb-item
-->

**Where the user is in a hierarchy, and the way back up.** Wraps a trail of
`md-breadcrumb-item` children in a labelled navigation region, keeps their
position data in sync, and can collapse the middle of a long trail behind an
overflow control.

> ⚠️ **Not a Material Design 3 component.** M3 has no breadcrumbs page; the
> Do / Don't below is house rules plus general navigation practice.

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

---

## When to use

- A **hierarchical** structure the user navigates into and back out of: file
  trees, category pages, nested settings.
- The path itself carries meaning and users need to jump to an ancestor.

## When NOT to use

| Situation | Use instead |
|---|---|
| A flat site with 2–3 destinations | `md-navigation-bar` / `md-tabs` |
| A linear process | `md-stepper` |
| Sibling views within a screen | `md-tabs` |
| Top-level navigation | `md-navigation-rail` / `md-navigation-bar` |
| Undo/back only | A back `md-icon-button` in `md-app-bar` |
| A single level deep | Nothing — the header says it |

## Decision cues

| Need | Setting |
|---|---|
| Show the whole trail, however long | `max-items="0"` (the default) |
| Collapse a long trail | `max-items="4"` |
| How many leading crumbs stay visible | `items-before-collapse` (default 1) |
| How many trailing crumbs stay visible | `items-after-collapse` (default 2, floor 1) |
| Custom separator glyph or text | `separator="›"` |
| An SVG separator | `::part(separator)` on both this and `md-breadcrumb-item` |
| Localized overflow control name | `expand-label` |
| Name the navigation region | `label` |
| Compact trail | `density="-1…-4"` |

## API contract

```html
<md-breadcrumbs
  label="Breadcrumb"                            <!-- default: Breadcrumb -->
  separator="/"                                 <!-- default: / -->
  max-items="4"                                 <!-- default: 0 — collapsing off -->
  items-before-collapse="1"                     <!-- default: 1 -->
  items-after-collapse="2"                      <!-- default: 2 -->
  expand-label="Show full breadcrumb trail"     <!-- default: Show full breadcrumb trail -->
  density="-1|-2|-3|-4"                         <!-- default: 0 — the uncompacted rung, which is inert -->
>
  <md-breadcrumb-item href="/">Home</md-breadcrumb-item>
  <md-breadcrumb-item href="/docs">Docs</md-breadcrumb-item>
  <md-breadcrumb-item>Buttons</md-breadcrumb-item>
</md-breadcrumbs>
```

**Events** — `mdExpand` with detail
`{ expanded: boolean; itemCount: number }`. Bubbles and is composed. The
visibility change has already happened when it fires, so `preventDefault()`
does nothing.

**Methods** — `expand() => Promise<void>`, `collapse() => Promise<void>`.

**Slots** — `(default)`: the `md-breadcrumb-item` children. There is **no**
`separator` slot — see below.

**Parts** — `list` (the `<ol>`), `overflow` (the "…" list item),
`overflow-button` (the button inside it), and `separator` — which on *this*
element matches only the separator drawn inside the overflow item. The
separators between crumbs belong to `md-breadcrumb-item`.

### Behavioral contract worth knowing

- **`max-items="0"` (the default) means no collapsing** — show everything. It
  does not mean "show none".
- Collapsing keeps the first `items-before-collapse` and the last
  `items-after-collapse` crumbs, with the overflow control between them. The
  after count is floored at **1**, so `items-after-collapse="0"` behaves as
  `1`. With `items-before-collapse="0"` the overflow's leading separator is
  dropped so nothing dangles at the start of the trail.
- If `items-before-collapse + items-after-collapse` is at least the number of
  crumbs, **no crumb is hidden but the overflow button still renders** —
  because the button's condition is only "more crumbs than `max-items`". Keep
  the two counts below the trail length.
- Activating the overflow control expands the trail and **removes the control
  from the DOM**; from then on the only way back to the collapsed view is the
  `collapse()` method.
- `expand()` / `collapse()` are no-ops when the trail is already in that state.
  Otherwise they flip it and emit `mdExpand` — including when `max-items="0"`,
  where nothing visible changes.
- **The last crumb is promoted to `current` automatically.** The component
  clears `current` from every other crumb — unless you explicitly set `current`
  on a crumb that is *not* last, which switches the auto-promotion off entirely
  and leaves your markup alone.
- Position data (`item-index`, `item-total`) is written onto every crumb by
  this component, and re-written on slot change and whenever the collapse props
  change. `md-breadcrumb-item` uses it to decide whether to draw its own
  leading separator.
- Only **direct** `md-breadcrumb-item` children are collected — crumbs
  forwarded through a wrapper component's `<slot>` are not seen. Render the
  items directly inside `md-breadcrumbs`.
- **There is no `separator` slot.** A single named slot can only project into
  one position in the shadow tree, and a trail needs an identical separator
  between *every* pair of crumbs — so each `md-breadcrumb-item` reads the
  parent's `separator` string and draws its own.
- Crumbs enter with a staggered fade-and-slide; the stagger is pre-baked for
  the first 12 items and the whole animation is disabled under
  `prefers-reduced-motion: reduce`.

---

## Do / Don't

House rules, informed by general navigation practice — no M3 breadcrumbs page
exists.

| ✅ Do | ❌ Don't |
|---|---|
| Let the last crumb be the current page, without an `href` | Don't make the current page a link |
| Keep crumb labels short — the page's own name | Don't repeat the full page title in each crumb |
| Collapse long trails with `max-items` | Don't wrap a long trail onto three lines |
| Keep the trail in real hierarchy order | Don't reorder crumbs by recency — that's history, not hierarchy |
| Localize `label` and `expand-label` | Don't ship the English defaults |
| Use breadcrumbs **in addition to** primary navigation | Don't make breadcrumbs the only way to navigate up |
| Pick a separator with clear visual meaning | Don't use a glyph that reads as content |
| Keep them near the top of the content region | Don't bury the trail mid-page |

---

## Patterns

```html
<md-breadcrumbs id="crumbs" label="Breadcrumb">
  <md-breadcrumb-item href="/" icon="home">Home</md-breadcrumb-item>
  <md-breadcrumb-item href="/library">Library</md-breadcrumb-item>
  <md-breadcrumb-item href="/library/data">Data</md-breadcrumb-item>
  <md-breadcrumb-item>Tables</md-breadcrumb-item>
</md-breadcrumbs>

<script type="module">
  const crumbs = document.getElementById('crumbs');
  crumbs.addEventListener('mdExpand', (e) => {
    console.log('expanded:', e.detail.expanded, 'of', e.detail.itemCount);
  });
</script>
```

```html
<!-- Deep trail: Home … Data / Tables -->
<md-breadcrumbs max-items="4" items-before-collapse="1" items-after-collapse="2">
  <md-breadcrumb-item href="/">Home</md-breadcrumb-item>
  <md-breadcrumb-item href="/a">Projects</md-breadcrumb-item>
  <md-breadcrumb-item href="/a/b">2026</md-breadcrumb-item>
  <md-breadcrumb-item href="/a/b/c">Q3</md-breadcrumb-item>
  <md-breadcrumb-item href="/a/b/c/d">Data</md-breadcrumb-item>
  <md-breadcrumb-item>Tables</md-breadcrumb-item>
</md-breadcrumbs>
```

```html
<!-- Chevron separator: a string prop, not a slot -->
<md-breadcrumbs separator="›">
  <md-breadcrumb-item href="/">Home</md-breadcrumb-item>
  <md-breadcrumb-item>Settings</md-breadcrumb-item>
</md-breadcrumbs>

<style>
  /* Or draw it yourself. Both parts are needed: the item renders the
     separators between crumbs, the parent renders the one in the
     overflow control. */
  md-breadcrumb-item::part(separator),
  md-breadcrumbs::part(separator) {
    font-size: 0;
    inline-size: 16px;
    block-size: 16px;
    background: center / contain no-repeat url('/chevron.svg');
  }
</style>
```

```html
<!-- Localized region and control names -->
<md-breadcrumbs label="Fil d'Ariane" expand-label="Afficher le chemin complet" max-items="4">
  <md-breadcrumb-item href="/">Accueil</md-breadcrumb-item>
  <md-breadcrumb-item href="/bibliotheque">Bibliothèque</md-breadcrumb-item>
  <md-breadcrumb-item>Tableaux</md-breadcrumb-item>
</md-breadcrumbs>
```

## Anti-patterns

| ❌ Wrong | ✅ Right | Why |
|---|---|---|
| `max-items="0"` expecting everything hidden | `0` disables collapsing | Same "0 means off" trap as elsewhere. |
| `items-before-collapse` + `items-after-collapse` ≥ the crumb count | Keep them smaller | Nothing gets hidden, yet the "…" button still renders. |
| `items-after-collapse="0"` to show only leading crumbs | Use `1` and mean it | The value is floored at 1. |
| Setting `current` on the last crumb "to be safe" | Leave it off | It is promoted automatically; setting it on a *non-last* crumb disables that promotion. |
| Giving the last crumb an `href` | Leave it link-less | It's the page you're already on. |
| Wrapping the crumbs in your own component's `<slot>` | Render them as direct children | Only direct children are collected. |
| Expecting the "…" button to re-collapse the trail | Call `collapse()` | The button is removed once expanded. |
| Breadcrumbs as the only up-navigation | Pair with real navigation | Not a primary nav pattern. |
| Trail built from browsing history | Use the real hierarchy | Breadcrumbs are structural. |
| Default English `label` / `expand-label` in a localized app | Translate them | They're the region and control names. |
| Breadcrumbs on a flat site | Drop them | They imply depth that isn't there. |

## Accessibility, RTL, density, i18n

**Accessibility**
- The host is `role="navigation"` named by `label` (default `"Breadcrumb"`) —
  translate it. Inside, the crumbs sit in an `<ol>`.
- The current page is exposed via `aria-current="page"` on the last crumb; that
  happens automatically, so don't reimplement it.
- Separators are `aria-hidden` and never announced.
- The overflow control is a real `<button>` with `aria-expanded` and the name
  from `expand-label`; give it a meaningful, localized value.
- Keep crumb order in the DOM identical to the visual order — the collapse
  logic reorders visually with CSS `order`, which does not change reading
  order.
- Under `forced-colors` the separator uses `GrayText` and the overflow button
  gains a `CanvasText` border.

**RTL** — set `dir="rtl"` on the element or an ancestor: the trail direction
mirrors, the entrance animation slides from the opposite side, and separator
glyphs get `unicode-bidi: plaintext`. A directional glyph (`›`, `→`) must be
swapped by you; a neutral `/` needs nothing.

**Density** — `density="-1…-4"` tightens the gap between crumbs and the
separator glyph size (crumb padding is on `md-breadcrumb-item`, which takes the
same rung). Only those four rungs exist. Rung `0` is the uncompacted default
and is inert — `density="0"` does **not** opt the trail out of an ancestor's
`data-density` rung; use `style="--md-sys-density-scale: 0"` for that (the
ancestor's `--md-sys-spacing-*` payload still inherits).

**i18n** — translate `label`, `expand-label`, and each crumb's text. Long
translated crumbs are exactly when `max-items` earns its keep.

## Related components

`md-breadcrumb-item` · `md-app-bar` · `md-tabs` · `md-navigation-rail` ·
`md-stepper` · `md-menu`

## Theming

| Custom property | Purpose | Default |
|---|---|---|
| `--md-breadcrumbs-gap` | Space between crumbs and separators | `calc(8px + density * 2px)` |
| `--md-breadcrumbs-separator-color` | Separator glyph colour | `--md-sys-color-on-surface-variant` |
| `--md-breadcrumbs-text-color` | Trail text colour (inherited by crumbs) | `--md-sys-color-on-surface-variant` |
| `--md-breadcrumbs-stagger` | Per-item entrance delay | `40ms` |

**CSS parts** — `list`, `overflow`, `overflow-button`, `separator` (the
overflow one only).

Per-crumb colours use the `--md-breadcrumb-item-*` properties documented on
`md-breadcrumb-item`.

```css
md-breadcrumbs {
  --md-breadcrumbs-gap: 4px;
  --md-breadcrumbs-separator-color: var(--md-sys-color-outline);
  --md-breadcrumbs-stagger: 0ms;
}
```

<!-- Auto Generated Below -->


## Properties

| Property              | Attribute               | Description                                                                                                                                                                                                                      | Type                        | Default                        |
| --------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- | ------------------------------ |
| `density`             | `density`               | Local density rung. Drives the same `--md-sys-density-scale` signal that a global `data-density` ancestor sets, so a local value simply overrides the inherited one. 0 = default, -4 = ultra-compact.                            | `-1 \| -2 \| -3 \| -4 \| 0` | `0`                            |
| `expandLabel`         | `expand-label`          | Accessible name for the overflow ("…") toggle button. Surface this in your i18n layer so screen-reader users in non-English locales hear a localized announcement when collapsing is in effect.                                  | `string`                    | `'Show full breadcrumb trail'` |
| `itemsAfterCollapse`  | `items-after-collapse`  | How many items to ALWAYS show at the end of the trail when collapsed. Default 2.                                                                                                                                                 | `number`                    | `2`                            |
| `itemsBeforeCollapse` | `items-before-collapse` | How many items to ALWAYS show at the start of the trail when collapsed. Default 1.                                                                                                                                               | `number`                    | `1`                            |
| `label`               | `label`                 | Accessible name. Defaults to "Breadcrumb".                                                                                                                                                                                       | `string`                    | `'Breadcrumb'`                 |
| `maxItems`            | `max-items`             | When the number of crumbs exceeds `maxItems`, the middle ones collapse behind an "…" toggle. Set to `0` to disable collapsing.                                                                                                   | `number`                    | `0`                            |
| `separator`           | `separator`             | Separator glyph placed between items. Use any single character or short string (e.g. `/`, `›`, `→`, `»`). For SVG / icon separators, override `::part(separator)` from the light DOM with a CSS `background-image` — see readme. | `string`                    | `'/'`                          |


## Events

| Event      | Description                                                                                                                                                                                                                                                                                                                                                                                        | Type                                     |
| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- |
| `mdExpand` | Fires when the user reveals the full breadcrumb trail by activating the overflow ("…") toggle, or when consumers programmatically call `expand()` / `collapse()`. Not cancelable — the visibility change has already happened. Use it for analytics or to react to a user who wants to see the full path.  Bubbles + composed, so consumers outside the shadow tree (the typical case) receive it. | `CustomEvent<MdBreadcrumbsExpandDetail>` |


## Methods

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

Programmatically re-collapse the trail to the configured
before/after window. No-op when the trail is already collapsed. When
`maxItems` is `0` (collapsing disabled) nothing visible changes, but the
state still flips and `mdExpand` still fires.

#### Returns

Type: `Promise<void>`



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

Programmatically reveal the full breadcrumb trail. No-op when the trail
is already expanded. When `maxItems` is `0` (collapsing disabled) nothing
visible changes, but the state still flips and `mdExpand` still fires.

#### Returns

Type: `Promise<void>`




## Shadow Parts

| Part                | Description |
| ------------------- | ----------- |
| `"list"`            |             |
| `"overflow"`        |             |
| `"overflow-button"` |             |


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

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