Skip to content

Navigation Rail

Top-level destinations on a large screen. A vertical rail of 3–7 destinations that can expand into a labelled drawer, host a FAB and a logo, and optionally behave modally. It owns the active index, the expansion state and roving focus across its md-navigation-rail-tab children.

Live preview Open in Storybook
Click the menu button to expand the rail.
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; block-size: 404px; inline-size: 100%; max-inline-size: 460px; border-radius: 16px; overflow: hidden;">
  <md-navigation-rail expandable label="Main navigation" active-index="0">
    <md-navigation-rail-tab icon="home" label="Home" value="home"></md-navigation-rail-tab>
    <md-navigation-rail-tab icon="folder" label="Files" value="files"></md-navigation-rail-tab>
    <md-navigation-rail-tab icon="settings" label="Settings" value="settings"></md-navigation-rail-tab>
  </md-navigation-rail>
  <main style="flex: 1; padding: 20px; background: var(--md-sys-color-surface-container-low); color: var(--md-sys-color-on-surface); font: 14px/1.6 system-ui;">
    Click the menu button to expand the rail.
  </main>
</div>

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


<md-navigation-rail></md-navigation-rail>
  • Top-level destinations on desktop / large layouts.
  • A persistent navigation surface that collapses to icons and expands to labels.
  • Navigation that hosts a primary action (a FAB) at the top.
SituationUse instead
Compact / mobile layouts, or bottom navigationmd-navigation-bar — the rail’s own orientation="horizontal" is a top-of-page bar for wide layouts, not a substitute
Sibling views inside one screenmd-tabs
A temporary set of actionsmd-menu
Supplementary panel contentmd-side-sheet
A linear flowmd-stepper
NeedSetting
Icons only, labels underneathvariant="standard" (default)
Icons beside labels, widervariant="expanded"
A user-driven toggle between themexpandable
Overlay on a scrim (styling only — no focus trap)modal
Where destinations sit verticallyalignment="top" / "middle" / "bottom"
Brand mark, primary action, accountlogo / fab / footer slots
Real links instead of buttonshref on the tab

standard is the spec’s collapsed rail: 80px wide, icon over label. expanded puts the icon and label inline in a 220–360px drawer.

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

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

<div style="display: flex; block-size: 364px; border-radius: 16px; overflow: hidden;">
  <md-navigation-rail variant="standard" label="Standard rail" active-index="0">
    <md-navigation-rail-tab icon="home" label="Home" value="home"></md-navigation-rail-tab>
    <md-navigation-rail-tab icon="folder" label="Files" value="files"></md-navigation-rail-tab>
    <md-navigation-rail-tab icon="settings" label="Settings" value="settings"></md-navigation-rail-tab>
  </md-navigation-rail>
</div>
<div style="display: flex; block-size: 364px; border-radius: 16px; overflow: hidden;">
  <md-navigation-rail variant="expanded" label="Expanded rail" active-index="0">
    <md-navigation-rail-tab icon="home" label="Home" value="home"></md-navigation-rail-tab>
    <md-navigation-rail-tab icon="folder" label="Files" value="files"></md-navigation-rail-tab>
    <md-navigation-rail-tab icon="settings" label="Settings" value="settings"></md-navigation-rail-tab>
  </md-navigation-rail>
</div>

expandable renders a built-in leading menu button that toggles between the two variants; its glyph animates from menu to menu_open. Give it a localized toggle-label. If you would rather supply your own control, leave expandable off and put a button in the header slot wired to expand() / collapse() / toggle().

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

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

<div style="display: flex; block-size: 384px; border-radius: 16px; overflow: hidden;">
  <md-navigation-rail expandable toggle-label="Toggle navigation" label="Mail" active-index="0">
    <md-navigation-rail-tab icon="inbox" label="Inbox" value="inbox" badge-value="3"></md-navigation-rail-tab>
    <md-navigation-rail-tab icon="send" label="Sent" value="sent"></md-navigation-rail-tab>
    <md-navigation-rail-tab icon="drafts" label="Drafts" value="drafts"></md-navigation-rail-tab>
  </md-navigation-rail>
</div>

The FAB goes in the fab slot at the top of the rail — M3 is explicit that it must not sit below the destinations. Two logo slots let you swap a mark (logo) for a wordmark (logo-expanded) across the transition. header holds a leading control, footer holds utilities or an account destination.

Logo, FAB and footer slots Open in Storybook
hexagon hexagon Acme
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; block-size: 584px; border-radius: 16px; overflow: hidden;">
  <md-navigation-rail expandable label="Acme" active-index="0">
    <span slot="logo" class="material-symbols-outlined" style="font-size: 28px; color: var(--md-sys-color-primary);">hexagon</span>
    <span slot="logo-expanded" style="display: inline-flex; align-items: center; gap: 8px; font-weight: 600;">
      <span class="material-symbols-outlined" style="font-size: 28px; color: var(--md-sys-color-primary);">hexagon</span>
      <span>Acme</span>
    </span>
    <md-fab slot="fab" icon="edit" label="Compose" aria-label="Compose"></md-fab>

    <md-navigation-rail-tab icon="inbox" label="Inbox" value="inbox"></md-navigation-rail-tab>
    <md-navigation-rail-tab icon="send" label="Sent" value="sent"></md-navigation-rail-tab>
    <md-navigation-rail-tab icon="schedule" label="Scheduled" value="scheduled"></md-navigation-rail-tab>

    <md-icon-button slot="footer" icon="help" aria-label="Help"></md-icon-button>
  </md-navigation-rail>
</div>

alignment pushes the destination group to the top (default), middle or bottom of the rail.

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

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

<div style="display: flex; block-size: 364px; border-radius: 16px; overflow: hidden;">
  <md-navigation-rail alignment="top" label="Top aligned" active-index="0">
    <md-navigation-rail-tab icon="home" label="Home" value="home"></md-navigation-rail-tab>
    <md-navigation-rail-tab icon="search" label="Search" value="search"></md-navigation-rail-tab>
  </md-navigation-rail>
</div>
<div style="display: flex; block-size: 364px; border-radius: 16px; overflow: hidden;">
  <md-navigation-rail alignment="middle" label="Middle aligned" active-index="0">
    <md-navigation-rail-tab icon="home" label="Home" value="home"></md-navigation-rail-tab>
    <md-navigation-rail-tab icon="search" label="Search" value="search"></md-navigation-rail-tab>
  </md-navigation-rail>
</div>
<div style="display: flex; block-size: 364px; border-radius: 16px; overflow: hidden;">
  <md-navigation-rail alignment="bottom" label="Bottom aligned" active-index="0">
    <md-navigation-rail-tab icon="home" label="Home" value="home"></md-navigation-rail-tab>
    <md-navigation-rail-tab icon="search" label="Search" value="search"></md-navigation-rail-tab>
  </md-navigation-rail>
</div>

label-visibility is all by default, which is what M3 recommends for 3–7 destinations. selected paints only the active label; none is icon-only. In every mode the tab’s label prop remains the accessible name, so never drop it.

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

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

<div style="display: flex; block-size: 344px; border-radius: 16px; overflow: hidden;">
  <md-navigation-rail label-visibility="all" label="Labels all" active-index="0">
    <md-navigation-rail-tab icon="home" label="Home" value="home"></md-navigation-rail-tab>
    <md-navigation-rail-tab icon="search" label="Search" value="search"></md-navigation-rail-tab>
    <md-navigation-rail-tab icon="settings" label="Settings" value="settings"></md-navigation-rail-tab>
  </md-navigation-rail>
</div>
<div style="display: flex; block-size: 344px; border-radius: 16px; overflow: hidden;">
  <md-navigation-rail label-visibility="selected" label="Labels selected" active-index="0">
    <md-navigation-rail-tab icon="home" label="Home" value="home"></md-navigation-rail-tab>
    <md-navigation-rail-tab icon="search" label="Search" value="search"></md-navigation-rail-tab>
    <md-navigation-rail-tab icon="settings" label="Settings" value="settings"></md-navigation-rail-tab>
  </md-navigation-rail>
</div>
<div style="display: flex; block-size: 344px; border-radius: 16px; overflow: hidden;">
  <md-navigation-rail label-visibility="none" label="Icons only" active-index="0">
    <md-navigation-rail-tab icon="home" label="Home" value="home"></md-navigation-rail-tab>
    <md-navigation-rail-tab icon="search" label="Search" value="search"></md-navigation-rail-tab>
    <md-navigation-rail-tab icon="settings" label="Settings" value="settings"></md-navigation-rail-tab>
  </md-navigation-rail>
</div>

badge switches on the dot; badge-value renders the pill. disabled removes the destination from the tab order.

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

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

<div style="display: flex; block-size: 384px; border-radius: 16px; overflow: hidden;">
  <md-navigation-rail label="Badges and states" active-index="0">
    <md-navigation-rail-tab icon="inbox" label="Inbox" value="inbox" badge-value="12"></md-navigation-rail-tab>
    <md-navigation-rail-tab icon="notifications" label="Alerts" value="alerts" badge></md-navigation-rail-tab>
    <md-navigation-rail-tab icon="archive" label="Archive" value="archive"></md-navigation-rail-tab>
    <md-navigation-rail-tab icon="delete" label="Trash" value="trash" disabled></md-navigation-rail-tab>
  </md-navigation-rail>
</div>

icon is also a slot, for artwork the Material Symbols names cannot express. Slot an <svg> (or an <img>) with slot="icon" in place of the icon prop — the destination sizes and colours it exactly like a symbol.

Slotted SVG glyphs Open in Storybook
content
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; block-size: 364px; border-radius: 16px; overflow: hidden;">
  <md-navigation-rail label="Slotted icons" active-index="0">
    <md-navigation-rail-tab label="Home" value="home">
      <svg slot="icon" viewBox="0 0 24 24" width="24" height="24" fill="currentColor"><path d="M12 3l9 8h-3v10h-5v-6h-2v6H6V11H3z"/></svg>
    </md-navigation-rail-tab>
    <md-navigation-rail-tab label="Starred" value="star">
      <svg slot="icon" viewBox="0 0 24 24" width="24" height="24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01z"/></svg>
    </md-navigation-rail-tab>
    <md-navigation-rail-tab icon="settings" label="Settings" value="settings"></md-navigation-rail-tab>
  </md-navigation-rail>
  <div style="flex:1;padding:16px;opacity:.6;font-size:.85rem;">content</div>
</div>

Three opt-in additions take the rail past a fixed vertical column. None of them change the default rail.

The same parts laid out as a top-of-page bar — logo leading, destinations in a row, FAB and footer trailing — for wide layouts where a side rail would spend width the content wants.

  • The bar sizes itself to its content, so no wrapper height is needed — unlike the vertical rail, which needs a bounded height or its destinations scroll.
  • expandable is ignored: a bar has nowhere to expand into, so the toggle is not rendered.
  • aria-orientation reports horizontal, and the arrow keys follow the inline axis, mirrored under dir="rtl".

Reports in the bar below is a menu trigger — and its dropdown nests one level further via md-sub-menu-item:

A horizontal rail — a top-of-page bar Open in Storybook
insights payments
Show code for each technology
<md-navigation-rail id="main-nav" orientation="horizontal" label="Main navigation" active-index="0">
<md-navigation-rail-tab icon="home" label="Home" value="home"></md-navigation-rail-tab>
<md-navigation-rail-tab icon="folder" label="Files" value="files"></md-navigation-rail-tab>
<md-navigation-rail-tab icon="star" label="Starred" value="starred"></md-navigation-rail-tab>
<md-navigation-rail-tab icon="groups" label="Team" value="team"></md-navigation-rail-tab>
<md-navigation-rail-tab icon="bar_chart" label="Reports" value="reports">
  <md-menu variant="vibrant" id="reports-menu" slot="submenu">
    <md-menu-item headline="Usage"><span slot="leading-icon" class="material-symbols-outlined">insights</span></md-menu-item>
    <md-menu-item headline="Revenue"><span slot="leading-icon" class="material-symbols-outlined">payments</span></md-menu-item>
  </md-menu>
</md-navigation-rail-tab>
<md-navigation-rail-tab icon="inbox" label="Inbox" value="inbox"></md-navigation-rail-tab>
<md-navigation-rail-tab icon="settings" label="Settings" value="settings"></md-navigation-rail-tab>
</md-navigation-rail>

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

// The rail owns the highlight — you only route. Fires for a plain
// destination *and* when a Reports row is chosen, which activates Reports.
nav.addEventListener('mdTabChange', (event) => {
  const { index, value } = event.detail;
  console.log('navigate to', value, '(index', index + ')');
});

// Which dropdown row was picked. md-menu-item's mdClick bubbles, so one
// listener on the menu covers every row.
document.getElementById('reports-menu').addEventListener('mdClick', (event) => {
  const row = event.target.closest('md-menu-item');
  console.log('open report:', row.getAttribute('headline'));
});
</script>

A destination in the bar can own a dropdown too — and that dropdown can itself nest, using md-sub-menu-item for a second level:

A bar destination with a nested dropdown Open in Storybook
dashboard sell extension analytics payments chat
Show code for each technology
<md-navigation-rail id="product-nav" orientation="horizontal" label="Product navigation" active-index="0">
<md-navigation-rail-tab icon="home" label="Home" value="home"></md-navigation-rail-tab>
<md-navigation-rail-tab icon="category" label="Products" value="products">
  <md-menu variant="vibrant" id="products-menu" slot="submenu">
    <md-menu-item headline="Overview"><span slot="leading-icon" class="material-symbols-outlined">dashboard</span></md-menu-item>
    <md-menu-item headline="Pricing"><span slot="leading-icon" class="material-symbols-outlined">sell</span></md-menu-item>
    <md-sub-menu-item headline="Integrations" supporting-text="Browse by type"><span slot="leading-icon" class="material-symbols-outlined">extension</span>
      <md-menu variant="vibrant" slot="submenu">
        <md-menu-item headline="Analytics"><span slot="leading-icon" class="material-symbols-outlined">analytics</span></md-menu-item>
        <md-menu-item headline="Payments"><span slot="leading-icon" class="material-symbols-outlined">payments</span></md-menu-item>
        <md-menu-item headline="Messaging"><span slot="leading-icon" class="material-symbols-outlined">chat</span></md-menu-item>
      </md-menu>
    </md-sub-menu-item>
  </md-menu>
</md-navigation-rail-tab>
<md-navigation-rail-tab icon="description" label="Docs" value="docs"></md-navigation-rail-tab>
<md-navigation-rail-tab icon="support" label="Support" value="support"></md-navigation-rail-tab>
</md-navigation-rail>

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

// The rail owns the highlight and guarantees exactly one active destination:
// activating Docs clears whatever row the Products dropdown had chosen.
nav.addEventListener('mdTabChange', (event) => {
  const { index, value } = event.detail;
  console.log('navigate to', value, '(index', index + ')');
});

// mdClick bubbles out of md-menu-item, so this one listener also catches the
// rows nested inside the Integrations sub-menu.
document.getElementById('products-menu').addEventListener('mdClick', (event) => {
  const row = event.target.closest('md-menu-item');
  const parent = row.closest('md-sub-menu-item');
  console.log('open', parent ? parent.getAttribute('headline') + ' / ' : '', row.getAttribute('headline'));
});
</script>

The bar’s three slots give the classic web-app header in one component: the logo slot leads, the destinations take the space between, and the footer slot trails — put the signed-in user’s avatar there and hang an md-menu off it. The destinations region grows to fill whatever the brand and account leave behind, so the items stay centred as the bar widens.

An application bar — brand, destinations, account menu Open in Storybook
hexagon Acme
account_circle person settings logout
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-navigation-rail orientation="horizontal" label="Main navigation" active-index="0">
  <span slot="logo" style="display: inline-flex; align-items: center; gap: 8px; font-weight: 600;">
    <span class="material-symbols-outlined" style="font-size: 28px; color: var(--md-sys-color-primary);">hexagon</span>
    <span>Acme</span>
  </span>

  <md-navigation-rail-tab icon="home" label="Home" value="home"></md-navigation-rail-tab>
  <md-navigation-rail-tab icon="dashboard" label="Dashboard" value="dashboard"></md-navigation-rail-tab>
  <md-navigation-rail-tab icon="folder" label="Projects" value="projects"></md-navigation-rail-tab>
  <md-navigation-rail-tab icon="bar_chart" label="Reports" value="reports"></md-navigation-rail-tab>

  <div slot="footer">
    <md-icon-button id="account-trigger" size="md" aria-haspopup="menu" aria-label="Account — Ada Lovelace">
      <md-avatar name="Ada Lovelace" initials="AL" size="40px"></md-avatar>
    </md-icon-button>
    <md-menu variant="vibrant" id="account-menu" anchor="account-trigger" placement="bottom-end">
      <md-menu-item headline="Ada Lovelace" supporting-text="ada@acme.example"><span slot="leading-icon" class="material-symbols-outlined">account_circle</span></md-menu-item>
      <md-divider></md-divider>
      <md-menu-item headline="Profile"><span slot="leading-icon" class="material-symbols-outlined">person</span></md-menu-item>
      <md-menu-item headline="Settings"><span slot="leading-icon" class="material-symbols-outlined">settings</span></md-menu-item>
      <md-divider></md-divider>
      <md-menu-item headline="Sign out"><span slot="leading-icon" class="material-symbols-outlined">logout</span></md-menu-item>
    </md-menu>
  </div>
</md-navigation-rail>

<script type="module">
  document.getElementById('account-trigger').addEventListener('click', () => {
    document.getElementById('account-menu').show();
  });
</script>

max-visible keeps N destinations in the rail and collapses the rest into an overflow trigger. Choosing one activates that destination, so the collapsed items stay first-class.

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

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

<div style="display: flex; block-size: 380px;">
  <md-navigation-rail label="With overflow" active-index="0" max-visible="3">
    <md-navigation-rail-tab icon="home" label="Home" value="home"></md-navigation-rail-tab>
    <md-navigation-rail-tab icon="folder" label="Files" value="files"></md-navigation-rail-tab>
    <md-navigation-rail-tab icon="star" label="Starred" value="starred"></md-navigation-rail-tab>
    <md-navigation-rail-tab icon="drafts" label="Drafts" value="drafts"></md-navigation-rail-tab>
    <md-navigation-rail-tab icon="settings" label="Settings" value="settings"></md-navigation-rail-tab>
  </md-navigation-rail>
</div>

Slot an md-menu into a destination’s submenu slot and it becomes a disclosure: activating it opens the dropdown rather than navigating, and the destination reports aria-haspopup="menu" / aria-expanded. The menu is anchored to the destination automatically.

Activating Products opens its menu instead of navigating Open in Storybook
dashboard sell history
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; block-size: 320px;">
  <md-navigation-rail label="With a dropdown" active-index="0">
    <md-navigation-rail-tab icon="home" label="Home" value="home"></md-navigation-rail-tab>
    <md-navigation-rail-tab icon="category" label="Products" value="products">
      <md-menu variant="vibrant" slot="submenu">
        <md-menu-item headline="Overview"><span slot="leading-icon" class="material-symbols-outlined">dashboard</span></md-menu-item>
        <md-menu-item headline="Pricing"><span slot="leading-icon" class="material-symbols-outlined">sell</span></md-menu-item>
        <md-menu-item headline="Changelog"><span slot="leading-icon" class="material-symbols-outlined">history</span></md-menu-item>
      </md-menu>
    </md-navigation-rail-tab>
    <md-navigation-rail-tab icon="settings" label="Settings" value="settings"></md-navigation-rail-tab>
  </md-navigation-rail>
</div>

modal restyles the expanded rail as a floating overlay with a scrim — it has no effect while collapsed, and positioning is the host app’s responsibility. It does not trap focus; the rail only collapses on Escape or a scrim click, returning focus to its toggle. full-height stretches the rail to 100dvh instead of filling its parent; use it when the rail is the app’s top-level side navigation.

Modal rail — collapsed on load, expand it to see the scrim Open in Storybook
Expand the rail — it floats over this content behind a scrim instead of pushing it.
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; block-size: 404px; inline-size: 100%; max-inline-size: 460px; border-radius: 16px; overflow: hidden; position: relative;">
  <md-navigation-rail modal expandable label="Modal rail" toggle-label="Toggle navigation" active-index="0">
    <md-navigation-rail-tab icon="home" label="Home" value="home"></md-navigation-rail-tab>
    <md-navigation-rail-tab icon="folder" label="Files" value="files"></md-navigation-rail-tab>
    <md-navigation-rail-tab icon="settings" label="Settings" value="settings"></md-navigation-rail-tab>
  </md-navigation-rail>
  <main style="flex: 1; padding: 20px; background: var(--md-sys-color-surface-container-low); color: var(--md-sys-color-on-surface); font: 14px/1.6 system-ui;">
    Expand the rail — it floats over this content behind a scrim instead of pushing it.
  </main>
</div>
EventCancelableDetailFires
mdTabChangeno{ index, value }The user selected a destination
mdExpandnovoidCollapsed → expanded
mdCollapsenovoidExpanded → collapsed

All three events bubble and are composed, so one delegated listener above the rail catches every one of them. Pick a destination, then work the toggle — mdTabChange fires only when the active index actually moves, so re-picking the current destination logs nothing:

All three events, caught by one delegated listener Open in Storybook
Pick a destination, then toggle the rail.
Show code for each technology
<md-navigation-rail id="rail" expandable toggle-label="Toggle navigation" label="Main navigation" active-index="0">
<md-navigation-rail-tab icon="home" label="Home" value="home"></md-navigation-rail-tab>
<md-navigation-rail-tab icon="folder" label="Files" value="files"></md-navigation-rail-tab>
<md-navigation-rail-tab icon="settings" label="Settings" value="settings"></md-navigation-rail-tab>
</md-navigation-rail>

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

// Route on detail.value — it survives destinations being reordered.
rail.addEventListener('mdTabChange', (e) => {
  console.log('navigate to', e.detail.value, 'at index', e.detail.index);
});

rail.addEventListener('mdExpand', () => console.log('expanded'));
rail.addEventListener('mdCollapse', () => console.log('collapsed'));
</script>

One step further: hand mdTabChange to your router and persist the expansion state, so the rail comes back the way the user left it:

<md-navigation-rail id="rail" expandable full-height active-index="0" label="Main navigation">
<md-fab slot="fab" icon="add" label="Create"></md-fab>

<md-navigation-rail-tab icon="home" label="Home" value="home"></md-navigation-rail-tab>
<md-navigation-rail-tab icon="folder" label="Files" value="files"></md-navigation-rail-tab>
<md-navigation-rail-tab icon="settings" label="Settings" value="settings"></md-navigation-rail-tab>
</md-navigation-rail>

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

rail.addEventListener('mdTabChange', (e) => router.go(e.detail.value));
rail.addEventListener('mdExpand', () => localStorage.setItem('railExpanded', '1'));
rail.addEventListener('mdCollapse', () => localStorage.removeItem('railExpanded'));

if (localStorage.getItem('railExpanded')) rail.expand();
</script>

Properties

PropertyAttributeTypeDefaultReflects
variantvariant'standard' | 'expanded''standard'Yes
modalmodalbooleanfalseYes
fullHeightfull-heightbooleanfalseYes
alignmentalignment'top' | 'middle' | 'bottom''top'Yes
expandableexpandablebooleanfalseYes
toggleLabeltoggle-labelstring'Toggle navigation'
labelVisibilitylabel-visibility'all' | 'selected' | 'none''all'Yes
activeIndexactive-indexnumber-1Yes
labellabelstring'Navigation'
disableFocusManagementdisable-focus-managementbooleanfalse
densitydensity0 | -1 | -2 | -3 | -40Yes
orientationorientation'vertical' | 'horizontal''vertical'Yes
maxVisiblemax-visiblenumber
overflowLabeloverflow-labelstring'More'
overflowIconoverflow-iconstring'more_horiz'

Methods

MethodParameters
expand()none
collapse()none
toggle()none
focusTab()index: number

Slots

SlotDescription
(default)
logoOptional brand/logo at the very top
logo-expanded
headerOptional top group (menu button, etc.)
fabOptional FAB above the destinations
footerOptional bottom group — settings, account / user menu, etc.

CSS Custom Properties

Override on the host element for per-instance theming:

PropertyDescription
--md-navigation-rail-container-colorRail background
--md-navigation-rail-container-widthWidth when collapsed (default 80px,
--md-navigation-rail-expanded-widthWidth when expanded (default 220px,
--md-navigation-rail-expanded-max-widthExpanded width cap (default 360px)
--md-navigation-rail-container-shapeBorder-radius
--md-navigation-rail-container-elevationBox-shadow when free-floating
--md-navigation-rail-padding-blockTop/bottom padding (default 44px top space)
--md-navigation-rail-padding-inlineStart/end padding
--md-navigation-rail-gapGap between rail sections
--md-navigation-rail-header-spaceMin space between header/FAB group and
--md-navigation-rail-destinations-gapGap between destinations (default 4px)
--md-navigation-rail-content-colorDefault foreground color
--md-navigation-rail-logo-icon-sizeBrand glyph footprint in the logo
--md-navigation-rail-horizontal-padding-block
--md-navigation-rail-horizontal-padding-inline

CSS Shadow Parts

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

PartDescription
scrim
containerThe flex column wrapping every section
logoLogo/brand slot wrapper (topmost)
logo-contracted
logo-expanded
toggle
headerHeader slot wrapper (top group)
fabFAB slot wrapper
destinationsThe role=tablist that holds destinations
overflow
overflow-trigger
footerFooter slot wrapper (bottom group)

The rail is composed from one sub-component, documented here rather than on its own page: it means nothing outside md-navigation-rail, which owns the active state, the expand transition and label visibility.

One destination in the rail — an icon, a label, the MD3 active-indicator pill, an optional badge, and a stable value used for routing.

  • active, expanded and label-visibility are rail-managed. Setting them by hand fights the parent — especially expanded, which the rail toggles during its expand/collapse transition.
  • value is the routing key. The rail’s mdTabChange reports it, and it is stabler than an index when destinations are added, removed or reordered.
  • Unlike md-navigation-tab there is no active-icon — the active state is conveyed by the indicator pill and colour, not a glyph swap.
  • badge is the boolean switch for the dot; badge-value renders the large badge.
  • With href the tab renders as an anchor (the anchor part) and uses aria-current="page" while active — don’t wrap it in an <a>.
  • mdTabClick { value } is the internal signal the rail listens to. Application code should listen for the rail’s mdTabChange.

Properties

PropertyAttributeTypeDefaultReflects
iconiconstring''
labellabelstring''
activeactivebooleanfalseYes
disableddisabledbooleanfalseYes
badgebadgebooleanfalse
badgeValuebadge-valuestring''
hrefhrefstring''
targettargetstring''
valuevaluestring''
labelVisibilitylabel-visibility'all' | 'selected' | 'none''all'Yes
expandedexpandedbooleanfalseYes
densitydensity0 | -1 | -2 | -3 | -40Yes

Methods

MethodParameters
clearSubmenuSelection()none

Slots

SlotDescription
iconReplace prop-based icon with custom SVG/font/component
submenu

CSS Custom Properties

Override on the host element for per-instance theming:

PropertyDescription
--md-navigation-rail-tab-icon-colorIcon color (inactive)
--md-navigation-rail-tab-active-icon-colorIcon color when active
--md-navigation-rail-tab-label-colorLabel color (inactive)
--md-navigation-rail-tab-active-label-colorLabel color when active
--md-navigation-rail-tab-indicator-colorActive-indicator pill color
--md-navigation-rail-tab-indicator-shapeIndicator border-radius
--md-navigation-rail-tab-indicator-widthIndicator pill width (default 56px)
--md-navigation-rail-tab-indicator-heightIndicator pill height (default 32px)
--md-navigation-rail-tab-icon-sizeIcon size (default 24px)
--md-navigation-rail-tab-min-heightTap target min height (default 56px)
--md-navigation-rail-tab-label-typographyComposite font shorthand for label
--md-navigation-rail-tab-badge-colorBadge background
--md-navigation-rail-tab-badge-content-colorBadge text color
--md-navigation-rail-tab-focus-ring-colorFocus outline color
--md-navigation-rail-tab-state-layer-colorHover/press state-layer color (on-secondary-container)
--md-navigation-rail-tab-focus-state-layer-opacityFocused state-layer opacity (default 0.10 per spec)
--md-navigation-rail-tab-press-state-layer-opacityPressed state-layer opacity (default 0.10 per spec)
--md-navigation-rail-tab-caret-duration

CSS Shadow Parts

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

PartDescription
iconThe Material Symbols icon
indicatorActive-indicator pill
state-layerHover/press overlay
icon-wrapperWrapper holding the icon and badge
labelLabel text
caret
anchorThe internal <a> element when `href` is set
  • label names the navigation landmark. Set something meaningful — “Main navigation” — for screen-reader users (WAI-ARIA APG / WCAG 2.4.1).
  • The rail manages roving focus across destinations unless you set disable-focus-management, in which case it is entirely your responsibility.
  • modal closes on Escape and on a scrim click, returning focus to the toggle — but it does not trap focus. Contain focus yourself (inert on the rest of the page between mdExpand and mdCollapse) if you need a true modal.
  • Exactly one destination is current; the indicator pill reflects it. A tab with href also gets aria-current="page" while active.
  • The expand/collapse toggle needs a localized toggle-label.
  • A decorative logo should have an empty alt; a meaningful one needs real text.
KeyAction
TabOne stop for the destination group, plus the toggle, FAB and footer controls
/ Move between destinations, wrapping at the ends
Home / EndFirst / last destination
Enter / SpaceActivate the focused destination
EscapeCloses a modal rail and returns focus to its trigger

Tab into the rail below: the whole destination group is one stop, and the arrows move within it. Home and End jump to the ends, and the toggle button is its own separate stop.

Roving focus across destinations Open in Storybook
Tab in, then arrow between destinations.
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; block-size: 384px; border-radius: 16px; overflow: hidden;">
  <md-navigation-rail label="Main navigation" expandable toggle-label="Toggle navigation" active-index="0">
    <md-navigation-rail-tab icon="home" label="Home" value="home"></md-navigation-rail-tab>
    <md-navigation-rail-tab icon="folder" label="Files" value="files" badge-value="3"></md-navigation-rail-tab>
    <md-navigation-rail-tab icon="settings" label="Settings" value="settings"></md-navigation-rail-tab>
  </md-navigation-rail>
  <div style="flex:1;padding:16px;opacity:.6;font-size:.85rem;">Tab in, then arrow between destinations.</div>
</div>

RTL — every box metric is logical, so the rail moves to the leading edge — the right — and its logo, indicator pill, labels and badges mirror with it under dir="rtl". Nothing is re-authored; only dir changes. See RTL.

<div dir="rtl">
<md-navigation-rail label="التنقل" active-index="0">
<md-navigation-rail-tab icon="home" label="الرئيسية" value="home"></md-navigation-rail-tab>
</md-navigation-rail>
</div>
The rail moves to the leading edge — right under dir=rtl Open in Storybook
ltr
content
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:14px 16px;align-items:center;">
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">ltr</span>
  <div dir="ltr" style="display:flex;block-size:324px;border-radius:16px;overflow:hidden;">
    <md-navigation-rail label="Main" active-index="0">
      <md-navigation-rail-tab icon="home" label="Home" value="home"></md-navigation-rail-tab>
      <md-navigation-rail-tab icon="folder" label="Files" value="files"></md-navigation-rail-tab>
      <md-navigation-rail-tab icon="settings" label="Settings" value="settings"></md-navigation-rail-tab>
    </md-navigation-rail>
    <div style="flex:1;padding:12px;opacity:.6;font-size:.8rem;">content</div>
  </div>
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">rtl</span>
  <div dir="rtl" style="display:flex;block-size:324px;border-radius:16px;overflow:hidden;">
    <md-navigation-rail label="التنقل" active-index="0">
      <md-navigation-rail-tab icon="home" label="الرئيسية" value="home"></md-navigation-rail-tab>
      <md-navigation-rail-tab icon="folder" label="الملفات" value="files"></md-navigation-rail-tab>
      <md-navigation-rail-tab icon="settings" label="الإعدادات" value="settings"></md-navigation-rail-tab>
    </md-navigation-rail>
    <div style="flex:1;padding:12px;opacity:.6;font-size:.8rem;">المحتوى</div>
  </div>
</div>

A vertical rail always reads on the block axis, so / are unaffected by dir. A horizontal rail reads on the inline axis, so the rail resolves the nearest explicit dir and mirrors the keys itself: under dir="rtl", ArrowRight moves toward the start of the bar and ArrowLeft toward the end. aria-orientation reports horizontal in both directions.

A horizontal rail — tab in, then arrow along each bar Open in Storybook
dir=ltr
dir=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;gap:20px;">
  <div dir="ltr">
    <span style="display:block;margin-block-end:8px;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">dir=ltr</span>
    <md-navigation-rail orientation="horizontal" label="Main navigation" active-index="0">
      <md-navigation-rail-tab icon="home" label="Home" value="home"></md-navigation-rail-tab>
      <md-navigation-rail-tab icon="folder" label="Files" value="files"></md-navigation-rail-tab>
      <md-navigation-rail-tab icon="settings" label="Settings" value="settings"></md-navigation-rail-tab>
    </md-navigation-rail>
  </div>
  <div dir="rtl">
    <span style="display:block;margin-block-end:8px;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">dir=rtl</span>
    <md-navigation-rail orientation="horizontal" label="التنقل" active-index="0">
      <md-navigation-rail-tab icon="home" label="الرئيسية" value="home"></md-navigation-rail-tab>
      <md-navigation-rail-tab icon="folder" label="الملفات" value="files"></md-navigation-rail-tab>
      <md-navigation-rail-tab icon="settings" label="الإعدادات" value="settings"></md-navigation-rail-tab>
    </md-navigation-rail>
  </div>
</div>

density="-1…-4" is a local override of the inherited data-density rung. It tapers the rail’s width (80px → 64px), its block padding, the gap between destinations and the glyph size, all in one signal. All five rungs, same rail:

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

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

<div style="display:flex;gap:16px;align-items:flex-start;flex-wrap:wrap;">
  <div style="display:flex;flex-direction:column;gap:8px;">
    <span style="opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">0</span>
    <div style="display:flex;block-size:344px;border-radius:16px;overflow:hidden;">
      <md-navigation-rail density="0" label="Density 0" active-index="0">
        <md-navigation-rail-tab icon="home" label="Home" value="home"></md-navigation-rail-tab>
        <md-navigation-rail-tab icon="folder" label="Files" value="files"></md-navigation-rail-tab>
        <md-navigation-rail-tab icon="settings" label="Settings" value="settings"></md-navigation-rail-tab>
      </md-navigation-rail>
    </div>
  </div>
  <div style="display:flex;flex-direction:column;gap:8px;">
    <span style="opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">-1</span>
    <div style="display:flex;block-size:344px;border-radius:16px;overflow:hidden;">
      <md-navigation-rail density="-1" label="Density -1" active-index="0">
        <md-navigation-rail-tab icon="home" label="Home" value="home"></md-navigation-rail-tab>
        <md-navigation-rail-tab icon="folder" label="Files" value="files"></md-navigation-rail-tab>
        <md-navigation-rail-tab icon="settings" label="Settings" value="settings"></md-navigation-rail-tab>
      </md-navigation-rail>
    </div>
  </div>
  <div style="display:flex;flex-direction:column;gap:8px;">
    <span style="opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">-2</span>
    <div style="display:flex;block-size:344px;border-radius:16px;overflow:hidden;">
      <md-navigation-rail density="-2" label="Density -2" active-index="0">
        <md-navigation-rail-tab icon="home" label="Home" value="home"></md-navigation-rail-tab>
        <md-navigation-rail-tab icon="folder" label="Files" value="files"></md-navigation-rail-tab>
        <md-navigation-rail-tab icon="settings" label="Settings" value="settings"></md-navigation-rail-tab>
      </md-navigation-rail>
    </div>
  </div>
  <div style="display:flex;flex-direction:column;gap:8px;">
    <span style="opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">-3</span>
    <div style="display:flex;block-size:344px;border-radius:16px;overflow:hidden;">
      <md-navigation-rail density="-3" label="Density -3" active-index="0">
        <md-navigation-rail-tab icon="home" label="Home" value="home"></md-navigation-rail-tab>
        <md-navigation-rail-tab icon="folder" label="Files" value="files"></md-navigation-rail-tab>
        <md-navigation-rail-tab icon="settings" label="Settings" value="settings"></md-navigation-rail-tab>
      </md-navigation-rail>
    </div>
  </div>
  <div style="display:flex;flex-direction:column;gap:8px;">
    <span style="opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">-4</span>
    <div style="display:flex;block-size:344px;border-radius:16px;overflow:hidden;">
      <md-navigation-rail density="-4" label="Density -4" active-index="0">
        <md-navigation-rail-tab icon="home" label="Home" value="home"></md-navigation-rail-tab>
        <md-navigation-rail-tab icon="folder" label="Files" value="files"></md-navigation-rail-tab>
        <md-navigation-rail-tab icon="settings" label="Settings" value="settings"></md-navigation-rail-tab>
      </md-navigation-rail>
    </div>
  </div>
</div>

Translation is where the expanded width actually gets tested — German compounds run far longer than their English source. M3 lets a long label wrap to two lines but forbids truncation, so check the widest locale you ship:

German labels in the expanded variant — where width bites Open in Storybook
Inhalt
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;block-size:364px;border-radius:16px;overflow:hidden;">
  <md-navigation-rail variant="expanded" label="Hauptnavigation" toggle-label="Navigation umschalten" active-index="0">
    <md-navigation-rail-tab icon="home" label="Startseite" value="home"></md-navigation-rail-tab>
    <md-navigation-rail-tab icon="folder" label="Dateiverwaltung" value="files"></md-navigation-rail-tab>
    <md-navigation-rail-tab icon="settings" label="Einstellungen" value="settings"></md-navigation-rail-tab>
  </md-navigation-rail>
  <div style="flex:1;padding:12px;opacity:.6;font-size:.8rem;">Inhalt</div>
</div>

Densitydensity="-1…-4" locally overrides the inherited data-density rung, and composes with direction without any extra wiring: a data-density ancestor and a dir="rtl" ancestor both just inherit in. See Density.

i18n — translate destination labels, label and toggle-label; lang and dir are inherited from any ancestor. Keep each tab’s value untranslated — it’s the routing key.

Custom propertyPurposeDefault
--md-navigation-rail-container-colorRail backgroundsurface
--md-navigation-rail-container-widthCollapsed width80px (density-aware)
--md-navigation-rail-expanded-width / -expanded-max-widthExpanded width220px / 360px
--md-navigation-rail-container-shape / -container-elevationSurface shape and shadow0px / none
--md-navigation-rail-padding-block / -padding-inline / -gapSpacing44px / 0px / 12px
--md-navigation-rail-destinations-gapGap between destinations8px (density-aware)
--md-navigation-rail-header-spaceSpace under the header/FAB group40px
--md-navigation-rail-content-colorDefault foregroundon-surface-variant
--md-navigation-rail-logo-icon-sizeBrand glyph footprint in the logo slots24px
--md-navigation-rail-horizontal-padding-block / -horizontal-padding-inlineBar insets under orientation="horizontal"0px / 12px

Destination appearance is themed with the --md-navigation-rail-tab-* properties — see md-navigation-rail-tab.

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

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

<div style="display: flex; block-size: 364px; border-radius: 16px; overflow: hidden;">
  <md-navigation-rail label="Themed rail" active-index="0" style="--md-navigation-rail-container-color: var(--md-sys-color-surface-container-highest); --md-navigation-rail-container-shape: 24px; --md-navigation-rail-destinations-gap: 16px;">
    <md-navigation-rail-tab icon="home" label="Home" value="home"></md-navigation-rail-tab>
    <md-navigation-rail-tab icon="folder" label="Files" value="files"></md-navigation-rail-tab>
    <md-navigation-rail-tab icon="settings" label="Settings" value="settings"></md-navigation-rail-tab>
  </md-navigation-rail>
</div>

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

Untouched defaults — follows the page theme Open in Storybook
content
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; block-size: 364px; border-radius: 16px; overflow: hidden;">
  <md-navigation-rail label="Untouched defaults" active-index="1">
    <md-navigation-rail-tab icon="home" label="Home" value="home"></md-navigation-rail-tab>
    <md-navigation-rail-tab icon="folder" label="Files" value="files" badge-value="3"></md-navigation-rail-tab>
    <md-navigation-rail-tab icon="settings" label="Settings" value="settings"></md-navigation-rail-tab>
  </md-navigation-rail>
  <div style="flex:1;padding:16px;opacity:.6;font-size:.85rem;">content</div>
</div>
Custom propertyPurpose
--md-navigation-rail-tab-icon-color / -active-icon-colorGlyph colour
--md-navigation-rail-tab-label-color / -active-label-colorLabel colour
--md-navigation-rail-tab-indicator-colorThe pill behind the active glyph
--md-navigation-rail-tab-indicator-width / -height / -shapeIndicator box
--md-navigation-rail-tab-icon-sizeGlyph size
--md-navigation-rail-tab-label-typographyLabel type
--md-navigation-rail-tab-min-heightDestination height
--md-navigation-rail-tab-badge-color / -badge-content-colorBadge fill / text
--md-navigation-rail-tab-state-layer-colorHover / focus / press overlay
--md-navigation-rail-tab-focus-ring-colorFocus ring
Tonal indicator and a branded rail
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;gap:16px;block-size:384px;">
  <div style="display:flex;block-size:364px;border-radius:16px;overflow:hidden;">
    <md-navigation-rail label="Tonal" active-index="1" style="--md-navigation-rail-tab-indicator-color: var(--md-sys-color-tertiary-container); --md-navigation-rail-tab-active-icon-color: var(--md-sys-color-on-tertiary-container); --md-navigation-rail-tab-active-label-color: var(--md-sys-color-on-tertiary-container); --md-navigation-rail-tab-indicator-shape: 8px;">
      <md-navigation-rail-tab icon="home" label="Home" value="h"></md-navigation-rail-tab>
      <md-navigation-rail-tab icon="folder" label="Files" value="f"></md-navigation-rail-tab>
      <md-navigation-rail-tab icon="settings" label="Settings" value="s"></md-navigation-rail-tab>
    </md-navigation-rail>
  </div>
  <div style="display:flex;block-size:364px;border-radius:16px;overflow:hidden;">
    <md-navigation-rail label="Branded" active-index="1" style="--md-navigation-rail-container-color: #f3e9ff; --md-navigation-rail-container-shape: 28px; --md-navigation-rail-tab-indicator-color: #7c4dff; --md-navigation-rail-tab-active-icon-color: #ffffff; --md-navigation-rail-tab-active-label-color: #311b92; --md-navigation-rail-tab-badge-color: #7c4dff;">
      <md-navigation-rail-tab icon="home" label="Home" value="h"></md-navigation-rail-tab>
      <md-navigation-rail-tab icon="folder" label="Files" value="f" badge-value="5"></md-navigation-rail-tab>
      <md-navigation-rail-tab icon="settings" label="Settings" value="s"></md-navigation-rail-tab>
    </md-navigation-rail>
  </div>
</div>
PartOnElement
containermd-navigation-railThe rail surface
scrimmd-navigation-railThe modal backdrop
logo / logo-contracted / logo-expandedmd-navigation-railBrand-mark wrappers per state
togglemd-navigation-railThe expand / collapse control
header / fab / footermd-navigation-railSlot wrappers
destinationsmd-navigation-railThe destination group
overflow / overflow-triggermd-navigation-railThe max-visible trigger and its wrapper
anchormd-navigation-rail-tabThe <a> / button box
icon-wrapper / iconmd-navigation-rail-tabGlyph wrapper / glyph
indicatormd-navigation-rail-tabThe active pill
labelmd-navigation-rail-tabLabel text
caretmd-navigation-rail-tabThe disclosure chevron on a submenu destination
state-layermd-navigation-rail-tabHover / focus / press overlay
container, destinations, label and indicator
content
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-rail::part(container) { border-inline-end: 2px solid var(--md-sys-color-primary); }
  .parts-rail::part(destinations) { gap: 18px; }
  .parts-rail md-navigation-rail-tab::part(label) { text-transform: uppercase; letter-spacing: .08em; font-size: 10px; }
  .parts-rail md-navigation-rail-tab::part(indicator) { border-radius: 6px; }
</style>
<div style="display: flex; block-size: 364px; border-radius: 16px; overflow: hidden;">
  <md-navigation-rail class="parts-rail" label="Styled parts" active-index="1">
    <md-navigation-rail-tab icon="home" label="Home" value="home"></md-navigation-rail-tab>
    <md-navigation-rail-tab icon="folder" label="Files" value="files"></md-navigation-rail-tab>
    <md-navigation-rail-tab icon="settings" label="Settings" value="settings"></md-navigation-rail-tab>
  </md-navigation-rail>
  <div style="flex:1;padding:16px;opacity:.6;font-size:.85rem;">content</div>
</div>
md-navigation-rail::part(destinations) {
gap: 12px;
}

md-navigation-rail-tab · md-navigation-bar · md-tabs · md-fab · md-side-sheet · md-app-bar · md-icon-button

For AI Agents — md-navigation-rail

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-navigation-rail 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-navigation-rail readme.md

# md-navigation-rail

<!-- llm:meta
tag: md-navigation-rail
category: navigation
status: md3-mapped
m3-guidelines: https://m3.material.io/components/navigation-rail/guidelines
form-associated: false
depends-on: md-icon-button, md-menu, md-menu-item
used-by: none
accepts-children: md-navigation-rail-tab
-->

**Top-level destinations on a large screen.** A rail that can expand to a
labelled drawer, host a FAB and a logo, and optionally float modally — laid out
**vertically** (the M3 default) or **horizontally** as a top-of-page
application bar.

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

---

## When to use

- Top-level destinations (3–7) on **desktop / large** layouts.
- A persistent navigation surface that can collapse to icons and expand to
  labels (`expandable`).
- Navigation that hosts a primary action (FAB) at the top.
- A top-of-page **application bar** — brand, destinations, account — via
  `orientation="horizontal"`.

## When NOT to use

| Situation | Use instead |
|---|---|
| Compact / mobile layouts | `md-navigation-bar`, docked to the bottom |
| Sibling views inside one screen | `md-tabs` |
| A title bar with page actions (back, search, overflow) | `md-app-bar` — a horizontal rail carries *destinations*, not page actions |
| A temporary set of commands | `md-menu` |
| Supplementary panel content | `md-side-sheet` |
| A linear flow | `md-stepper` |

## Decision cues

| Need | Setting |
|---|---|
| Icons with labels underneath | `variant="standard"` (default) |
| Expanded drawer look | `variant="expanded"` |
| User can toggle between them | `expandable` (+ `toggle-label`) |
| Expanded rail floats over the content | `modal` |
| Rail spans the viewport height | `full-height` |
| Push destinations down/centre | `alignment="middle"` / `"bottom"` |
| Show labels only for the active item | `label-visibility="selected"` |
| Your own focus handling | `disable-focus-management` |
| A top-of-page bar instead of a side rail | `orientation="horizontal"` |
| Too many destinations for the width | `max-visible="N"` (+ `overflow-label`, `overflow-icon`) |
| A destination that opens a dropdown | `md-menu` in the tab's `submenu` slot |
| Brand at the leading edge | `slot="logo"` (+ `slot="logo-expanded"`) |
| Signed-in user at the trailing edge | `slot="footer"` |

**Choosing an orientation**

| Window | Use |
|---|---|
| Compact (< 600px) | `md-navigation-bar`, docked to the bottom |
| Medium / expanded, content is wide (tables, canvases, editors) | `orientation="horizontal"` — a bar spends height, not the width the content wants |
| Medium / expanded, content is narrow or reading-width | `orientation="vertical"` (default) — the M3 rail |

## API contract

```html
<md-navigation-rail
  orientation="vertical|horizontal"   <!-- default: vertical -->
  variant="standard|expanded"         <!-- default: standard -->
  expandable
  toggle-label="Toggle navigation"    <!-- default: "Toggle navigation" -->
  modal
  full-height
  alignment="top|middle|bottom"       <!-- default: top -->
  label-visibility="all|selected|none"<!-- default: all -->
  active-index="0"                    <!-- default: -1 (nothing selected) -->
  label="Main navigation"             <!-- default: "Navigation" -->
  max-visible="5"                     <!-- default: unset (no overflow) -->
  overflow-label="More"               <!-- default: "More" -->
  overflow-icon="more_horiz"          <!-- default: more_horiz -->
  disable-focus-management
  density="-1|-2|-3|-4"               <!-- default: 0 (uncompacted) -->
>
  <img slot="logo" src="/mark.svg" alt="">
  <img slot="logo-expanded" src="/wordmark.svg" alt="Acme">
  <md-fab slot="fab" icon="add" label="Create"></md-fab>

  <md-navigation-rail-tab icon="home"   label="Home"   value="home"></md-navigation-rail-tab>
  <md-navigation-rail-tab icon="search" label="Search" value="search"></md-navigation-rail-tab>

  <div slot="footer">…</div>
</md-navigation-rail>
```

**Events** — `mdTabChange` `{ index, value }`, `mdExpand` (no detail),
`mdCollapse` (no detail). All bubble and are composed.

**Methods** — `expand()`, `collapse()`, `toggle()`, `focusTab(index)`.

**Slots** — the default slot (destinations: `md-navigation-rail-tab`
children), `logo`, `logo-expanded`, `header`, `fab`, `footer`.

**Parts** — `scrim`, `container`, `logo`, `logo-contracted`, `logo-expanded`,
`toggle`, `header`, `fab`, `destinations`, `overflow`, `overflow-trigger`,
`footer`.

### Behavioral contract worth knowing

- **`active-index` defaults to `-1`** — nothing selected. Either set it, or mark
  one child with the `active` attribute: on first load (and only while
  `active-index` is still negative) the rail adopts that child's index.
- `mdTabChange` fires **only when the index actually moves**. Re-activating the
  current destination replays the ripple and emits nothing.
- `mdTabChange` carries both `index` and `value`; `value` comes from the child's
  `value` prop and is the stabler key for routing.
- The rail writes `active`, `label-visibility`, `expanded`, `data-orientation`
  and `tabindex` onto every destination on each sync. Setting those on a child
  yourself does not survive.
- A `md-fab` in the `fab` slot is **morphed by the rail**: it sets the FAB's
  `extended` property from the rail's expanded state. Don't pin `extended`
  yourself.
- `expand()` / `collapse()` / `toggle()` change `variant`, which is what emits
  `mdExpand` / `mdCollapse` — so setting `variant` directly emits them too.
- **`expandable` is ignored while `orientation="horizontal"`** — a bar has
  nowhere to grow, so no toggle button is rendered.
- **`modal` is a presentation mode, not a dialog.** Expanded, the rail's
  container becomes an absolutely-positioned overlay with a 32%-opacity scrim,
  while the host keeps its collapsed 80px footprint so the page never reflows.
  `Escape` and a scrim click collapse it, and focus returns to the **built-in**
  toggle button — which is only rendered when `expandable` is also set on a
  vertical rail. A `modal` rail driven from your own control in the `header`
  slot leaves focus on `<body>` after a dismiss, so restore it yourself from
  `mdCollapse`. It
  does **not** trap focus and does not make the page inert — the scrim is
  positioned against the rail's nearest positioned ancestor, so that ancestor
  needs `position: relative`.
- `full-height` switches the rail from filling its container to `100dvh`.
- With `max-visible="N"`, destinations past N get `data-overflowed` and move
  into a menu behind an overflow trigger. Overflowed destinations are skipped by
  the arrow keys, exactly like disabled ones, and choosing one from the menu
  still activates it and emits `mdTabChange`.
- The **overflow trigger is a `button` with `aria-haspopup="menu"`, deliberately
  outside the `tablist`** — a tablist may only contain tabs.
- **If any destination sets `href`, the destinations container drops its
  `tablist` role** (and its `aria-orientation`), because a tablist may only
  contain `tab` children. It stays inside the `navigation` landmark.
- A destination with a slotted `submenu` is a **disclosure, not a target**:
  activating it opens the dropdown instead of selecting. Choosing a *row* is
  what selects the destination — that emits `mdTabChange` and clears any row
  chosen in another destination's dropdown, so two destinations never read as
  current at once.
- The rail drops its `isolation: isolate` while **any** slotted `md-menu` is
  open (a destination's dropdown, or an account menu in the `footer`);
  otherwise the fixed-position menu would be trapped in the rail's stacking
  context and painted over.
- Keyboard: `ArrowUp` / `ArrowDown` always move between destinations;
  `ArrowLeft` / `ArrowRight` do too, following the writing direction when
  `orientation="horizontal"`. `Home` / `End` jump to the ends, and movement
  wraps. Focus moves without selecting — `Enter` / `Space` on a destination is
  what activates it.
- `disable-focus-management` turns off the roving tabindex and the arrow keys
  entirely; you own keyboard navigation from that point.

---

## Do / Don't

Sourced from [M3 · Navigation rail · Guidelines](https://m3.material.io/components/navigation-rail/guidelines).
The horizontal layout is this library's extension, so the rows about it are
house rules.

| ✅ Do | ❌ Don't |
|---|---|
| Keep 3–7 destinations; cap the rest with `max-visible` | Don't let destinations squeeze until labels truncate |
| Reach for `orientation="horizontal"` only on wide layouts, where a side rail would spend width the content wants | Don't ship a horizontal rail on compact — that's `md-navigation-bar`, at the bottom |
| Keep a horizontal rail to *destinations* | Don't fill it with page actions — that's `md-app-bar` |
| Ship one top-level navigation surface per screen | Don't stack a rail above an app bar that also navigates |
| Place the FAB at the **top** of the rail | Avoid placing the FAB below the navigation items |
| Use the active indicator for the one current page | Don't indicate more than one item at a time |
| Slot a `md-menu` into `submenu` for sub-*destinations* | Don't put commands (Cut, Copy, Export) in a destination's dropdown |
| Write clear, concise labels describing the destination | Don't truncate or show an ellipsis in place of label text |
| Break a longer phrase into two lines if needed | Don't reduce the type size to fit more characters |
| Be careful with logos in the rail | Don't place a logo where it reads as an action or destination |
| Use a rail on desktop, a bar on compact | Don't ship a bar on desktop |

---

## Patterns

```html
<md-navigation-rail id="rail" expandable full-height active-index="0" label="Main navigation">
  <img slot="logo" src="/mark.svg" alt="">
  <md-fab slot="fab" icon="add" label="Create"></md-fab>

  <md-navigation-rail-tab icon="home"     label="Home"     value="home"></md-navigation-rail-tab>
  <md-navigation-rail-tab icon="folder"   label="Files"    value="files"></md-navigation-rail-tab>
  <md-navigation-rail-tab icon="settings" label="Settings" value="settings"></md-navigation-rail-tab>
</md-navigation-rail>

<script type="module">
  const rail = document.getElementById('rail');
  rail.addEventListener('mdTabChange', (e) => router.go(e.detail.value));  // prefer value
  rail.addEventListener('mdExpand',   () => localStorage.setItem('railExpanded', '1'));
  rail.addEventListener('mdCollapse', () => localStorage.removeItem('railExpanded'));
</script>
```

```html
<!-- Modal rail: the overlay is positioned against the nearest positioned
     ancestor, so give the layout `position: relative`. -->
<div style="position: relative; display: flex; min-block-size: 100dvh;">
  <md-navigation-rail id="modal-rail" modal expandable label="Main navigation">
    <md-navigation-rail-tab icon="home"   label="Home"   value="home"></md-navigation-rail-tab>
    <md-navigation-rail-tab icon="folder" label="Files"  value="files"></md-navigation-rail-tab>
  </md-navigation-rail>
  <main>…</main>
</div>
```

```html
<!-- Destinations pushed to the middle, utilities in the footer -->
<md-navigation-rail alignment="middle" label="Main navigation">
  <md-navigation-rail-tab icon="home"  label="Home"  value="home"></md-navigation-rail-tab>
  <md-navigation-rail-tab icon="inbox" label="Inbox" value="inbox"></md-navigation-rail-tab>
  <div slot="footer">
    <md-icon-button icon="help" aria-label="Help"></md-icon-button>
  </div>
</md-navigation-rail>
```

```html
<!-- Application bar: brand leads, destinations take the middle, account trails -->
<md-navigation-rail orientation="horizontal" label="Main navigation" active-index="0">
  <span slot="logo">…brand…</span>

  <md-navigation-rail-tab icon="home"      label="Home"      value="home"></md-navigation-rail-tab>
  <md-navigation-rail-tab icon="dashboard" label="Dashboard" value="dashboard"></md-navigation-rail-tab>

  <!-- A destination that discloses instead of navigating -->
  <md-navigation-rail-tab icon="bar_chart" label="Reports" value="reports">
    <md-menu slot="submenu" variant="vibrant">
      <md-menu-item headline="Usage"></md-menu-item>
      <md-menu-item headline="Revenue"></md-menu-item>
    </md-menu>
  </md-navigation-rail-tab>

  <div slot="footer">
    <md-icon-button id="account" size="md" aria-haspopup="menu" aria-label="Account">
      <md-avatar name="Ada Lovelace" initials="AL" size="40px"></md-avatar>
    </md-icon-button>
    <md-menu id="account-menu" anchor="account" placement="bottom-end" variant="vibrant">
      <md-menu-item headline="Profile"></md-menu-item>
      <md-menu-item headline="Sign out"></md-menu-item>
    </md-menu>
  </div>
</md-navigation-rail>

<script type="module">
  document.getElementById('account').addEventListener('click', () => {
    document.getElementById('account-menu').show();
  });
</script>
```

```html
<!-- Overflow: keep four in the bar, collapse the rest into a menu -->
<md-navigation-rail orientation="horizontal" max-visible="4" overflow-label="More"
                    label="Main navigation">
  <md-navigation-rail-tab icon="home"      label="Home"      value="home"></md-navigation-rail-tab>
  <md-navigation-rail-tab icon="dashboard" label="Dashboard" value="dashboard"></md-navigation-rail-tab>
  <md-navigation-rail-tab icon="folder"    label="Files"     value="files"></md-navigation-rail-tab>
  <md-navigation-rail-tab icon="group"     label="Team"      value="team"></md-navigation-rail-tab>
  <md-navigation-rail-tab icon="settings"  label="Settings"  value="settings"></md-navigation-rail-tab>
</md-navigation-rail>
```

## Anti-patterns

| ❌ Wrong | ✅ Right | Why |
|---|---|---|
| Leaving `active-index` at `-1` | Set it, or mark one child `active` | The default is "nothing selected". |
| Routing on `detail.index` | Route on `detail.value` | Indices shift when destinations change. |
| Waiting for `mdTabChange` when the user re-clicks the current destination | Don't — it does not fire | The event is selection-*changed*, not activation. |
| Pinning `extended` on the slotted FAB | Let the rail morph it | The rail drives that transition. |
| A horizontal rail on compact | `md-navigation-bar` | M3: compact gets a bottom bar. |
| `expandable` on a horizontal rail | Drop it | Ignored — a bar cannot expand. |
| Routing off a click on a destination that owns a submenu | Route off `mdTabChange` | That click only opens the dropdown; choosing a row is the selection. |
| More destinations than fit, left to squeeze | `max-visible` | Overflow keeps the collapsed ones reachable and first-class. |
| `modal` used where a focus-trapping dialog is needed | `md-dialog`, or trap focus yourself | The modal rail dims and dismisses, but does not trap focus. |
| `modal` inside a `position: static` layout | Give the layout `position: relative` | The overlay and scrim are absolutely positioned against the nearest positioned ancestor. |
| FAB below the destinations | Top of the rail | M3 explicit rule. |
| A clickable logo that looks like a destination | Make it clearly distinct | M3 caution; the `logo` slot sits outside the `tablist`. |
| Truncated labels | Two lines, or shorter words | M3 explicit rule. |
| `disable-focus-management` without replacing it | Leave focus management on | You would ship a keyboard-inaccessible rail. |
| `md-navigation-tab` children | `md-navigation-rail-tab` | Different component; the rail only syncs its own tag. |

## Accessibility, RTL, density, i18n

**Accessibility**
- The host is a `navigation` landmark named by `label` — set it to something
  meaningful ("Main navigation"). It defaults to "Navigation".
- The destinations container is a `tablist` with `aria-orientation` matching
  `orientation`, unless a destination is a link — see the behavioral contract.
- The rail manages roving focus across destinations unless you set
  `disable-focus-management`, in which case it is entirely your responsibility.
- `modal` dims the page and dismisses on `Escape` / scrim click, returning
  focus to the toggle button. It does not trap focus — add a trap yourself if
  the interaction demands one.
- Exactly one destination is current; the rail clears the others, including a
  row chosen inside another destination's dropdown.
- The expand/collapse toggle carries `aria-expanded` and needs a localized
  `toggle-label`.
- A decorative logo should have empty `alt`; a meaningful one needs real text.
- The overflow trigger is a `button` with `aria-haspopup="menu"` and a
  localized `overflow-label`, kept outside the `tablist`.
- Anything in `logo` / `header` / `fab` / `footer` is outside the `tablist` —
  an account avatar needs its own accessible name (and `aria-haspopup="menu"`
  when it opens one).

**RTL** — the rail sits on the leading edge and all internals use logical
properties. In a horizontal rail the left/right arrow keys follow the writing
direction, so `ArrowRight` moves toward the start under `dir="rtl"`.

**Density** — `density="-1…-4"` narrows the rail (80 → 56px collapsed), tightens
the destination gap and the block padding. Rung `0` is the uncompacted default
and is inert. To opt a rail out of an inherited global `data-density` rung, set
`style="--md-sys-density-scale: 0"` on it.

**i18n** — translate destination labels, `label`, `toggle-label` and
`overflow-label`; keep each destination's `value` untranslated. Long labels can
wrap to two lines (M3 allows this) but must not truncate — check the expanded
width per locale.

## Related components

`md-navigation-rail-tab` · `md-navigation-bar` · `md-tabs` · `md-fab` ·
`md-side-sheet` · `md-app-bar` · `md-menu` · `md-icon-button`

## Theming

| Custom property | Purpose | Default |
|---|---|---|
| `--md-navigation-rail-container-color` | Rail background | `--md-sys-color-surface` (`surface-container` when `modal` and expanded) |
| `--md-navigation-rail-content-color` | Default foreground | `--md-sys-color-on-surface-variant` |
| `--md-navigation-rail-container-width` | Collapsed width | `80px` (tapers 4px/rung, floor 56px) |
| `--md-navigation-rail-expanded-width` | Expanded width | `220px` (tapers 20px/rung, floor 140px) |
| `--md-navigation-rail-expanded-max-width` | Expanded width cap | `360px` |
| `--md-navigation-rail-container-shape` | Corner radius | `0px` (`corner-large` when `modal` and expanded) |
| `--md-navigation-rail-container-elevation` | Box-shadow | `none` (`elevation-3` when `modal` and expanded) |
| `--md-navigation-rail-padding-block` | Block padding | `44px` (tapers 4px/rung, floor 24px) |
| `--md-navigation-rail-padding-inline` | Inline padding while collapsed | `0px` — the expanded rail hard-codes a 12px inset and ignores this |
| `--md-navigation-rail-gap` | Gap between sections | `--md-sys-spacing-gap-md` (12px) |
| `--md-navigation-rail-header-space` | Space under the header/FAB group | `40px` (tapers 4px/rung, floor 20px) |
| `--md-navigation-rail-destinations-gap` | Gap between destinations | `8px` (tapers 2px/rung, floor 0) |
| `--md-navigation-rail-logo-icon-size` | Brand glyph footprint in the logo slots | `24px` |
| `--md-navigation-rail-horizontal-height` | Bar height when `orientation="horizontal"` | `72px` (tapers 4px/rung, floor 64px) |
| `--md-navigation-rail-horizontal-padding-block` | Bar block gutter | `0px` (so a destination's ripple reaches the edges) |
| `--md-navigation-rail-horizontal-padding-inline` | Bar inline gutter | `--md-sys-spacing-inset-md` (12px) |

**CSS parts** — `scrim`, `container`, `logo`, `logo-contracted`,
`logo-expanded`, `toggle`, `header`, `fab`, `destinations`, `overflow`,
`overflow-trigger`, `footer`.

Destination appearance is themed with the `--md-navigation-rail-tab-*`
properties.

```css
md-navigation-rail.brand {
  --md-navigation-rail-container-color: var(--md-sys-color-surface-container);
  --md-navigation-rail-expanded-width: 280px;
}
```

<!-- Auto Generated Below -->


## Overview

Material Design 3 — Navigation Rail

A vertical navigation surface for the left or right edge of medium / large
window sizes. Holds 3–7 top-level destinations and optionally a header
(menu / brand), a FAB, and a footer.

Implements MD3 specs:
  - https://m3.material.io/components/navigation-rail/overview
  - https://m3.material.io/components/navigation-rail/specs
  - https://m3.material.io/components/navigation-rail/guidelines
  - https://m3.material.io/components/navigation-rail/accessibility

## Properties

| Property                 | Attribute                  | Description                                                                                                                                                                                                                                                                                                                                                                                   | Type                            | Default               |
| ------------------------ | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | --------------------- |
| `activeIndex`            | `active-index`             | Index of the currently active destination. -1 means no destination is active. Two-way bound: updates when the user selects a destination and can be set externally to programmatically change the active destination.                                                                                                                                                                         | `number`                        | `-1`                  |
| `alignment`              | `alignment`                | Vertical alignment of destinations relative to the rail.                                                                                                                                                                                                                                                                                                                                      | `"bottom" \| "middle" \| "top"` | `'top'`               |
| `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`                   |
| `disableFocusManagement` | `disable-focus-management` | Set to `true` to disable focus management (roving tabindex + arrow keys). Use when embedding inside a custom focus management system.                                                                                                                                                                                                                                                         | `boolean`                       | `false`               |
| `expandable`             | `expandable`               | Render a built-in leading menu button that toggles between the collapsed (`standard`) and `expanded` variants. The icon animates between `menu` (collapsed) and `menu_open` (expanded) per the MD3 navigation rail figure. When you'd rather supply your own control, leave this `false` and place a button in the `header` slot wired to the `expand()` / `collapse()` / `toggle()` methods. | `boolean`                       | `false`               |
| `fullHeight`             | `full-height`              | Stretch the rail to the full viewport height (`100dvh`) instead of filling its parent container. Opt-in: by default the rail fills whatever height its container gives it (`100%`), which keeps it embeddable in split-pane / nested / modal-docked layouts. Set this when the rail is the app's top-level side navigation and should always span the screen.                                 | `boolean`                       | `false`               |
| `label`                  | `label`                    | Accessible name announced for the rail's `navigation` landmark. Required for screen reader users (WAI-ARIA APG / WCAG 2.4.1).                                                                                                                                                                                                                                                                 | `string`                        | `'Navigation'`        |
| `labelVisibility`        | `label-visibility`         | Controls whether destination labels are shown.  - `all`: every destination shows its label (default — recommended by MD3 for 3–7 destinations)  - `selected`: only the active destination shows its label  - `none`: icons only                                                                                                                                                               | `"all" \| "none" \| "selected"` | `'all'`               |
| `maxVisible`             | `max-visible`              | Cap on how many destinations stay in the rail. Any beyond it collapse into an overflow trigger that opens a menu of the rest; choosing one activates that destination. Leave unset for no overflow. The trigger is a `button` with `aria-haspopup="menu"`, deliberately OUTSIDE the `tablist`.                                                                                                  | `number \| undefined`           | `undefined`           |
| `modal`                  | `modal`                    | Modal styling for the expanded variant per spec: `surface-container` background, level-3 elevation, large corner shape. Use when the expanded rail floats over content instead of being docked. The rail renders its own overlay container **and its own 32%-opacity scrim** (clicking it collapses the rail), positioned against the nearest positioned ancestor. No effect while collapsed.                                                                                                       | `boolean`                       | `false`               |
| `orientation`            | `orientation`              | Layout axis. `vertical` is the M3 rail on the leading edge; `horizontal` lays the same parts out as a top-of-page bar (logo leading, destinations in a row, FAB and footer trailing). The expand/collapse affordance is vertical-only, so `expandable` is ignored while horizontal.                                                                                                             | `"horizontal" \| "vertical"`    | `'vertical'`          |
| `overflowIcon`           | `overflow-icon`            | Material Symbols ligature for the overflow trigger.                                                                                                                                                                                                                                                                                                                                          | `string`                        | `'more_horiz'`        |
| `overflowLabel`          | `overflow-label`           | Accessible name and label for the overflow trigger. Localize per page.                                                                                                                                                                                                                                                                                                                       | `string`                        | `'More'`              |
| `toggleLabel`            | `toggle-label`             | Accessible label for the built-in toggle button (only rendered when `expandable` is set). Announced to assistive tech as the button's name.                                                                                                                                                                                                                                                   | `string`                        | `'Toggle navigation'` |
| `variant`                | `variant`                  | Visual variant — `standard` (the spec's *collapsed* rail: 80px wide, stacked icon + label) or `expanded` (icons + label inline, 220–360px wide).                                                                                                                                                                                                                                              | `"expanded" \| "standard"`      | `'standard'`          |


## Events

| Event         | Description                                                  | Type                                             |
| ------------- | ------------------------------------------------------------ | ------------------------------------------------ |
| `mdCollapse`  | Emitted when the rail transitions from expanded → collapsed. | `CustomEvent<void>`                              |
| `mdExpand`    | Emitted when the rail transitions from collapsed → expanded. | `CustomEvent<void>`                              |
| `mdTabChange` | Emitted when the user selects a destination.                 | `CustomEvent<{ index: number; value: string; }>` |


## Methods

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

Programmatically collapse the rail (standard 80px width).

#### Returns

Type: `Promise<void>`



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

Programmatically expand the rail (shows labels inline with icons).

#### Returns

Type: `Promise<void>`



### `focusTab(index: number) => Promise<void>`

Move keyboard focus to the destination at `index`.

#### Parameters

| Name    | Type     | Description |
| ------- | -------- | ----------- |
| `index` | `number` |             |

#### Returns

Type: `Promise<void>`



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

Toggle between expanded and standard.

#### Returns

Type: `Promise<void>`




## Shadow Parts

| Part                | Description |
| ------------------- | ----------- |
| `"container"`       |             |
| `"destinations"`    |             |
| `"fab"`             |             |
| `"footer"`          |             |
| `"header"`          |             |
| `"logo"`            |             |
| `"logo-contracted"` |             |
| `"logo-expanded"`   |             |
| `"overflow"`        |             |
| `"overflow-trigger"`|             |
| `"scrim"`           |             |
| `"toggle"`          |             |


## Dependencies

### Depends on

- [md-icon-button](../md-icon-button)
- [md-menu](../md-menu)
- [md-menu-item](../md-menu-item)

### Graph
```mermaid
graph TD;
  md-navigation-rail --> md-icon-button
  md-navigation-rail --> md-menu
  md-navigation-rail --> md-menu-item
  md-icon-button --> md-ripple
  style md-navigation-rail fill:#f9f,stroke:#333,stroke-width:4px
```

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

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

AWC UI Operator’s Manual

# main-llm.md — AWC UI build director

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

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

Your job, in order:

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

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

---

## §1 — Interview the user

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

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

### 1.1 Scope and shape

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

### 1.2 Look and feel

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

### 1.3 Internationalization

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

### 1.4 Data and forms

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

### 1.5 Constraints

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

---

## §2 — Map answers to configuration

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

### 2.1 Global switches — the complete set

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

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

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

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

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

---

## §3 — Install and bootstrap

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

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

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

defineCustomElements(window);
```

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

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

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

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

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

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

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

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

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

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

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

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

---

## §4 — Global configuration reference

### 4.1 The token system

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

#### Color roles

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

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

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

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

#### Shape tokens

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

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

#### Elevation tokens

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

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

#### Motion tokens

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

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

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

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

#### State-layer opacities

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

#### Typography, spacing and layering

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

### 4.2 Theming and rebranding

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

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

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

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

### 4.3 Density

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

Two details that bite:

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

### 4.4 Dark mode

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

### 4.5 RTL

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

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

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

### 4.6 Internationalization

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

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

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

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

### 4.7 Forms and validation

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

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

Rules:

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

---

## §5 — Component decision matrix

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

### 5.1 Actions

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

### 5.2 Text input

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

### 5.3 Selection

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

### 5.4 Navigation

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

### 5.5 Containment and feedback

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

### 5.6 Data

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

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

### 5.7 Choosing the variant

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

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

### 5.8 Not in the library

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

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

---

## §6 — Component inventory

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

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

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

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

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

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

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

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

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

---

## §7 — Composition rules

### 7.1 What nests inside what

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

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

### 7.2 Pairs that belong together

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

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

### 7.3 Nesting that is always wrong

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

---

## §8 — Page recipes

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

### 8.1 Login screen

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

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

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

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

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

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

### 8.2 Settings page (mobile)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

### 8.4 Dashboard with a FAB

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

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

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

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

### 8.5 Destructive confirmation dialog

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

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

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

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

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

### 8.6 Tabbed content page

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

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

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

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

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

### 8.7 The full recipe library

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

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

---

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

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

### 9.1 API and styling

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

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

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

### 9.2 Content and hierarchy

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

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

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

### 9.3 Accessibility contract

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

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

---

## §10 — Before you ship

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