Skip to content

List

A vertical set of related rows. md-list owns the list semantics, selection mode, roving keyboard focus and optional drag-reordering; the rows themselves are md-list-item children. Configuration flows down from the list — you set it once on the wrapper, never on the rows.

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

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

<md-list label="Mail" style="inline-size: 340px;">
  <md-list-item type="button" headline="Inbox" supporting-text="12 new" leading-icon="inbox" trailing-supporting-text="24"></md-list-item>
  <md-divider inset-start></md-divider>
  <md-list-item type="button" headline="Starred" supporting-text="Marked important" leading-icon="star" trailing-supporting-text="3"></md-list-item>
  <md-divider inset-start></md-divider>
  <md-list-item type="button" headline="Drafts" supporting-text="Resume where you left off" leading-icon="drafts"></md-list-item>
  <md-divider inset-start></md-divider>
  <md-list-item type="button" headline="Sent" supporting-text="Outgoing mail" leading-icon="send"></md-list-item>
</md-list>

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


<md-list></md-list>
  • A vertical collection of similar records: contacts, files, messages, settings.
  • Rows that may be selectable, activatable, or reorderable.
  • Content that reads top-to-bottom rather than comparing across columns.
SituationUse instead
Comparable data across columnsmd-table
Rich, self-contained items with their own actionsA md-card collection
A contextual popup of actionsmd-menu
Options in a pickermd-select / md-multi-select
Collapsible content sectionsmd-accordion
Navigation destinationsmd-navigation-rail / md-navigation-bar
Moving items between two poolsmd-transfer-list

selectionMode, interactionMode and reorderable are written onto the rows by the list. density gets there a different way — the list declares --md-sys-density-scale on its own host and every row inherits it — but the rule for you is the same: set it on the wrapper. Setting a written prop on an individual md-list-item fights the parent and desynchronises the roving-focus model. Keep md-list-item as a direct child too — wrapping rows in <div>s for layout breaks child discovery.

Not one row below carries an interaction, reorder or density attribute — the drag handles, the separate focus stops for the trailing buttons and the tighter rung all come from the three attributes on the wrapper:

Configuration flows down — the rows declare none of 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>

<md-list interaction-mode="multi-action" reorderable density="-1" label="Configured by the list" style="inline-size: 360px;">
  <md-list-item type="button" headline="Ada Lovelace" supporting-text="Engineering" leading-icon="person">
    <md-icon-button slot="trailing" icon="mail" aria-label="Email Ada Lovelace"></md-icon-button>
  </md-list-item>
  <md-list-item type="button" headline="Alan Turing" supporting-text="Research" leading-icon="person">
    <md-icon-button slot="trailing" icon="mail" aria-label="Email Alan Turing"></md-icon-button>
  </md-list-item>
  <md-list-item type="button" headline="Grace Hopper" supporting-text="Compilers" leading-icon="person">
    <md-icon-button slot="trailing" icon="mail" aria-label="Email Grace Hopper"></md-icon-button>
  </md-list-item>
</md-list>
StyleLookUse for
standardDefault. One shared rounded chassis, no gaps — interleave dividers for separatorsDense, continuous lists
segmentedSeparate rounded tiles with a small gap; each row reads as its own affordanceGrouped settings, pickers
Standard versus segmented Open in Storybook
Show code for each technology
<!-- index.html <head> — the icon font the components draw from -->
<link rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">

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

<md-list list-style="standard" label="Account settings" style="inline-size: 260px;">
  <md-list-item type="button" headline="Profile" leading-icon="person"></md-list-item>
  <md-divider inset-start></md-divider>
  <md-list-item type="button" headline="Privacy" leading-icon="lock"></md-list-item>
  <md-divider inset-start></md-divider>
  <md-list-item type="button" headline="Sign out" leading-icon="logout"></md-list-item>
</md-list>
<md-list list-style="segmented" label="Library" style="inline-size: 260px;">
  <md-list-item type="button" headline="Photos" leading-icon="photo" trailing-supporting-text="128"></md-list-item>
  <md-list-item type="button" headline="Videos" leading-icon="videocam" trailing-supporting-text="42"></md-list-item>
  <md-list-item type="button" headline="Music" leading-icon="music_note" trailing-supporting-text="256"></md-list-item>
</md-list>

There is no dividers prop. A separator is an md-divider element you interleave between rows yourself — inset-start is the usual choice, because it starts the rule past the leading-icon column:

The same three rows, with and without interleaved md-divider elements 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; gap: 24px; flex-wrap: wrap;">
  <md-list label="With dividers" style="inline-size: 260px;">
    <md-list-item headline="Inbox" leading-icon="inbox"></md-list-item>
    <md-divider inset-start></md-divider>
    <md-list-item headline="Starred" leading-icon="star"></md-list-item>
    <md-divider inset-start></md-divider>
    <md-list-item headline="Archive" leading-icon="archive"></md-list-item>
  </md-list>
  <md-list label="Without dividers" style="inline-size: 260px;">
    <md-list-item headline="Inbox" leading-icon="inbox"></md-list-item>
    <md-list-item headline="Starred" leading-icon="star"></md-list-item>
    <md-list-item headline="Archive" leading-icon="archive"></md-list-item>
  </md-list>
</div>

A row promotes itself to two or three lines automatically when you add an overline, supporting-text, or both. You don’t set a lines value by hand for the common cases.

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

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

<md-list label="Row anatomy" style="inline-size: 380px;">
  <md-list-item type="button" headline="One line"></md-list-item>
  <md-divider inset-start></md-divider>
  <md-list-item type="button" headline="Two lines" supporting-text="Supporting text promotes the row" leading-icon="inbox"></md-list-item>
  <md-divider inset-start></md-divider>
  <md-list-item type="button" headline="Overline also promotes" overline="OVERLINE" leading-icon="label"></md-list-item>
  <md-divider inset-start></md-divider>
  <md-list-item type="button" headline="Full configuration" supporting-text="Headline, supporting text and trailing meta" leading-icon="star" trailing-supporting-text="5 min ago"></md-list-item>
</md-list>

A one-line row is the floor — headline only, no second region:

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

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

<md-list label="One line" style="max-inline-size: 340px;">
  <md-list-item headline="Wi-Fi" trailing-icon="chevron_right"></md-list-item>
  <md-list-item headline="Bluetooth" trailing-icon="chevron_right"></md-list-item>
</md-list>

And an overline on top of headline + supporting-text promotes the row all the way to three lines:

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

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

<md-list label="Three line" style="max-inline-size: 400px;">
  <md-list-item lines="3" overline="Today" headline="Deployment finished" supporting-text="All 14 services are healthy and traffic has been shifted to the new revision." leading-icon="rocket_launch"></md-list-item>
  <md-list-item lines="3" overline="Yesterday" headline="Nightly backup" supporting-text="Snapshot completed in 42 seconds and was copied to the secondary region." leading-icon="backup"></md-list-item>
</md-list>

type decides whether a row is inert, a button, or a link.

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

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

<md-list label="Row types" style="inline-size: 400px;">
  <md-list-item type="text" headline="Static text row" supporting-text="Not focusable — no ripple, no click event" leading-icon="text_fields"></md-list-item>
  <md-divider inset-start></md-divider>
  <md-list-item type="button" headline="Button row" supporting-text="Focusable, ripples on press, emits mdClick" leading-icon="touch_app" trailing-icon="chevron_right"></md-list-item>
  <md-divider inset-start></md-divider>
  <md-list-item type="link" href="https://m3.material.io/components/lists/guidelines" target="_blank" headline="Link row" supporting-text="Navigates to href in target" leading-icon="open_in_new" trailing-icon="north_east"></md-list-item>
</md-list>

M3 puts supporting visuals at the leading edge, because that is what makes a list scannable. An icon, a 40px avatar and a square thumbnail are all first-class props.

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

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

<md-list label="Leading content" style="inline-size: 380px;">
  <md-list-item type="button" headline="Leading icon" supporting-text="24px Material Symbol" leading-icon="wifi" trailing-icon="chevron_right"></md-list-item>
  <md-divider inset-start></md-divider>
  <md-list-item type="button" headline="Jane Cooper" supporting-text="Hey, are you free tonight?" leading-avatar="https://i.pravatar.cc/80?img=1" leading-avatar-alt="Jane Cooper" trailing-supporting-text="5m"></md-list-item>
  <md-divider inset-start></md-divider>
  <md-list-item type="button" headline="Leading image" supporting-text="Square thumbnail" leading-image="https://picsum.photos/seed/mdlist/112/112" leading-image-alt=""></md-list-item>
</md-list>

leading-avatar takes an image URL; leading-avatar-name takes a name and renders the initials instead, which is the fallback to reach for when a contact has no photo:

Initial avatars and image thumbnails Open in Storybook
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<div style="display: flex; gap: 24px; flex-wrap: wrap;">
  <md-list label="Avatars" style="inline-size: 300px;">
    <md-list-item lines="2" headline="Ada Lovelace" supporting-text="Engineering" leading-avatar-name="Ada Lovelace"></md-list-item>
    <md-list-item lines="2" headline="Alan Turing" supporting-text="Research" leading-avatar-name="Alan Turing"></md-list-item>
  </md-list>
  <md-list label="Thumbnails" style="inline-size: 300px;">
    <md-list-item lines="2" headline="Coastline" supporting-text="4.2 MB" leading-image="https://picsum.photos/seed/a/80/80" leading-image-alt=""></md-list-item>
    <md-list-item lines="2" headline="Forest" supporting-text="3.1 MB" leading-image="https://picsum.photos/seed/b/80/80" leading-image-alt=""></md-list-item>
  </md-list>
</div>

This is the distinction the API turns on:

EventMeans
mdActivateThe active row changed — the roving focus target moved, by click or by arrow key
mdSelectThe selected set changed

A list can do one, both, or neither. selection-mode="none" (the default) is an activatable list: rows open things and nothing stays ticked.

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

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

<md-list selection-mode="single-select" label="Plan" style="inline-size: 260px;">
  <md-list-item type="button" headline="Free" leading-icon="check_circle"></md-list-item>
  <md-list-item type="button" headline="Pro" leading-icon="check_circle" selected></md-list-item>
  <md-list-item type="button" headline="Team" leading-icon="check_circle"></md-list-item>
  <md-list-item type="button" headline="Enterprise" leading-icon="check_circle"></md-list-item>
</md-list>
<md-list selection-mode="multi-select" label="Notification channels" style="inline-size: 260px;">
  <md-list-item type="button" headline="Email" selected></md-list-item>
  <md-list-item type="button" headline="SMS"></md-list-item>
  <md-list-item type="button" headline="Push" selected></md-list-item>
  <md-list-item type="button" headline="In-app banner"></md-list-item>
</md-list>
Multi-select, and selection inside a segmented list Open in Storybook
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<div style="display: flex; gap: 24px; flex-wrap: wrap;">
  <md-list label="Multi-select" selection-mode="multi-select" style="inline-size: 280px;">
    <md-list-item headline="Photos" selected></md-list-item>
    <md-list-item headline="Videos" selected></md-list-item>
    <md-list-item headline="Documents"></md-list-item>
  </md-list>
  <md-list label="Segmented selection" list-style="segmented" selection-mode="single-select" style="inline-size: 280px;">
    <md-list-item headline="Light" selected></md-list-item>
    <md-list-item headline="Dark"></md-list-item>
    <md-list-item headline="System"></md-list-item>
  </md-list>
</div>
Thumbnail selection Open in Storybook
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-list label="Pick artwork" selection-mode="multi-select" style="max-inline-size: 340px;">
  <md-list-item lines="2" headline="Coastline" supporting-text="1600 × 900" leading-image="https://picsum.photos/seed/c/80/80" leading-image-alt="" selected></md-list-item>
  <md-list-item lines="2" headline="Forest" supporting-text="1600 × 900" leading-image="https://picsum.photos/seed/d/80/80" leading-image-alt=""></md-list-item>
  <md-list-item lines="2" headline="Desert" supporting-text="1600 × 900" leading-image="https://picsum.photos/seed/e/80/80" leading-image-alt=""></md-list-item>
</md-list>

single-select moves the selection; multi-select sets aria-multiselectable on the list and toggles rows independently. Read the result with getSelectedIndices().

interaction-mode="multi-action" tells rows they contain more than one control, which changes their keyboard model: trailing buttons get their own focus stops and a click on one does not activate the row’s primary action. Set it whenever rows carry trailing controls — leaving it at single-action gives the row the wrong keyboard model.

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

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

<md-list interaction-mode="multi-action" label="Files" style="inline-size: 380px;">
  <md-list-item type="button" headline="report.pdf" supporting-text="2.4 MB" leading-icon="picture_as_pdf">
    <md-icon-button slot="trailing" icon="bookmark_border" aria-label="Bookmark report.pdf"></md-icon-button>
    <md-icon-button slot="trailing" icon="more_vert" aria-label="More actions for report.pdf"></md-icon-button>
  </md-list-item>
  <md-divider inset-start></md-divider>
  <md-list-item type="button" headline="budget.xlsx" supporting-text="812 KB" leading-icon="table_chart">
    <md-icon-button slot="trailing" icon="bookmark_border" aria-label="Bookmark budget.xlsx"></md-icon-button>
    <md-icon-button slot="trailing" icon="more_vert" aria-label="More actions for budget.xlsx"></md-icon-button>
  </md-list-item>
</md-list>

Trailing supporting text is a prop; anything richer goes in the trailing slot, and the same applies to leading, headline and supporting-text:

Trailing supporting text, and slotted regions Open in Storybook
Mentions you Two unread threads
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: 24px; flex-wrap: wrap;">
  <md-list label="Trailing text" style="inline-size: 300px;">
    <md-list-item lines="2" headline="Storage" supporting-text="Documents" trailing-supporting-text="12.4 GB"></md-list-item>
    <md-list-item lines="2" headline="Backups" supporting-text="Weekly" trailing-supporting-text="3.1 GB"></md-list-item>
  </md-list>
  <md-list label="Slotted regions" style="inline-size: 320px;">
    <md-list-item lines="2">
      <span slot="leading" class="material-symbols-outlined" aria-hidden="true">alternate_email</span>
      <span slot="headline">Mentions <strong>you</strong></span>
      <span slot="supporting-text">Two unread threads</span>
      <md-switch slot="trailing" aria-label="Notify me about mentions"></md-switch>
    </md-list-item>
  </md-list>
</div>

Note the accessible names: “More actions for report.pdf”, not “More actions”. In a list of ten rows the bare label is useless.

A row marked expandable reveals slot="expanded-content" children inline below it, with an automatic caret. The children are flat rows, not a nested sub-list, and a child selection re-emits on the list’s mdSelect carrying both the parent index and the childIndex.

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

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

<md-list selection-mode="single-select" label="Expansion list" style="inline-size: 340px;">
  <md-list-item type="button" headline="Shared with me" leading-icon="folder_shared" expandable expanded>
    <md-list-item slot="expanded-content" type="button" headline="Q3 planning" leading-icon="description"></md-list-item>
    <md-list-item slot="expanded-content" type="button" headline="Design review" leading-icon="description" selected></md-list-item>
    <md-list-item slot="expanded-content" type="button" headline="Budget draft" leading-icon="description"></md-list-item>
  </md-list-item>
  <md-divider inset-start></md-divider>
  <md-list-item type="button" headline="Recent" leading-icon="schedule" expandable>
    <md-list-item slot="expanded-content" type="button" headline="notes.txt" leading-icon="description"></md-list-item>
    <md-list-item slot="expanded-content" type="button" headline="agenda.md" leading-icon="description"></md-list-item>
  </md-list-item>
</md-list>

Expandable rows inside a segmented list behave the same way, with each group keeping its own corner radii.

reorderable gives every row a trailing drag handle and emits mdReorder on drop. The list persists nothing. e.detail.order is the new order expressed as the original authored indices, so order[newPosition] is the old position — one pass is enough to reorder a backing array.

Reordering is not pointer-only: with focus on a row, Alt + ArrowUp / Alt + ArrowDown move it one position and emit the same mdReorder. The list re-focuses the row at its new position, so a run of keystrokes keeps working without re-aiming.

Drag a row's trailing handle — or focus a row and press Alt + ArrowUp / ArrowDown Open in Storybook
Show code for each technology
<!-- index.html <head> — the icon font the components draw from -->
<link rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">

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

<md-list reorderable interaction-mode="multi-action" label="Playlist" style="inline-size: 360px;">
  <md-list-item type="button" headline="Midnight City" supporting-text="M83" leading-icon="music_note"></md-list-item>
  <md-list-item type="button" headline="Instant Crush" supporting-text="Daft Punk" leading-icon="music_note"></md-list-item>
  <md-list-item type="button" headline="The Less I Know the Better" supporting-text="Tame Impala" leading-icon="music_note"></md-list-item>
</md-list>
Enabled, disabled and soft-disabled rows Open in Storybook
Show code for each technology
<!-- index.html <head> — the icon font the components draw from -->
<link rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">

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

<md-list label="Row states" style="inline-size: 340px;">
  <md-list-item type="button" headline="Enabled" supporting-text="Focusable and clickable" leading-icon="check"></md-list-item>
  <md-divider inset-start></md-divider>
  <md-list-item type="button" headline="Disabled" supporting-text="Out of the tab order" leading-icon="block" disabled></md-list-item>
  <md-divider inset-start></md-divider>
  <md-list-item type="button" headline="Soft-disabled" supporting-text="Still reachable by keyboard" leading-icon="lock" soft-disabled></md-list-item>
</md-list>
Long text — lines sets the height, it does not truncate Open in Storybook
Show code for each technology
<!-- index.html <head> — the icon font the components draw from -->
<link rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">

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

<md-list label="Long content" style="max-inline-size: 340px;">
  <md-list-item lines="3" headline="A headline long enough that it has to wrap onto a second line in this column" supporting-text="Supporting text that also runs well past the available width and keeps going." leading-icon="subject"></md-list-item>
  <md-list-item lines="1" headline="A one-line row whose headline is far too long for the space it has been given"></md-list-item>
</md-list>

The second row is the trap: lines="1" does not clip text, it only sizes the row for one line.

Everything above shows one prop at a time. A real list is a combination — here segmented gives each preference its own tile, multi-action gives the switches their own focus stops, and each switch names the row it controls:

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

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

<md-list list-style="segmented" interaction-mode="multi-action" label="Notification preferences" style="inline-size: 380px;">
  <md-list-item lines="2" headline="Email" supporting-text="A digest, once a day" leading-icon="mail">
    <md-switch slot="trailing" selected aria-label="Email notifications"></md-switch>
  </md-list-item>
  <md-list-item lines="2" headline="Push" supporting-text="Instant, on this device" leading-icon="notifications">
    <md-switch slot="trailing" selected aria-label="Push notifications"></md-switch>
  </md-list-item>
  <md-list-item lines="2" headline="SMS" supporting-text="Security alerts only" leading-icon="sms">
    <md-switch slot="trailing" aria-label="SMS notifications"></md-switch>
  </md-list-item>
</md-list>

The multi-action Files demo further up is the same idea applied to a file manager: multi-action for the trailing controls, row-scoped names on each of them, and the row’s own primary action left intact.

EventCancelableDetailFires
mdSelectnoMdListSelectDetailThe selected set changed
mdActivateno{ index, item }The active (roved) row changed — click or arrow key
mdReorderno{ from, to, order }A drag-reorder completed
type MdListSelectDetail = {
index: number; // top-level row index (parent index for expanded children)
value: string;
selected: boolean;
item: HTMLMdListItemElement;
childIndex?: number; // index within the parent's expanded-content slot
expanded?: boolean; // whether that expandable parent is open
};

MethodsactivateNext(), activatePrevious(), selectItem(index), getSelectedIndices(). All four are async.

Click a row, arrow-key between rows, and move one with a drag handle or with Alt + ArrowUp / Alt + ArrowDown — all three events land in the same log:

Selection, activation and reorder — watch the log
Click a row to select it, arrow-key between rows, or move one with a drag handle or Alt + ArrowUp / ArrowDown.
Show code for each technology
<md-list id="files" label="Files" selection-mode="multi-select" interaction-mode="multi-action" reorderable>
<md-list-item type="button" headline="report.pdf"></md-list-item>
<md-list-item type="button" headline="budget.xlsx"></md-list-item>
<md-list-item type="button" headline="notes.txt"></md-list-item>
</md-list>

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

list.addEventListener('mdSelect', async (e) => {
  console.log(e.detail.index, e.detail.value, e.detail.selected);
  console.log('selected set', await list.getSelectedIndices());
});

list.addEventListener('mdActivate', (e) => {
  console.log('active row', e.detail.index);
});

list.addEventListener('mdReorder', (e) => {
  console.log(e.detail.from, e.detail.to, e.detail.order);
});
</script>

The demo above only reads the events. The next step is keeping a backing array in sync, which is the one thing the markup cannot express — order[newPosition] is the old position, so a single map re-sorts the data:

<md-list id="files" label="Files" selection-mode="multi-select" interaction-mode="multi-action" reorderable>
<md-list-item type="button" headline="report.pdf"></md-list-item>
<md-list-item type="button" headline="budget.xlsx"></md-list-item>
</md-list>

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

list.addEventListener('mdSelect', async () => {
  console.log(await list.getSelectedIndices());
});

list.addEventListener('mdActivate', (e) => {
  console.log('active row', e.detail.index);
});

// The list reports the new order — you persist it.
list.addEventListener('mdReorder', (e) => {
  const { order } = e.detail;
  files = order.map((originalIndex) => files[originalIndex]);
  save(files);
});
</script>

Properties

PropertyAttributeTypeDefaultReflects
listStylelist-styleMdListStyle'standard'
selectionModeselection-modeMdListSelectionMode'none'
interactionModeinteraction-modeMdListInteractionMode'single-action'
reorderablereorderablebooleanfalseYes
labellabelstring''
labelledbylabelledbystring''
roleOverriderole-overridestring''
densitydensity0 | -1 | -2 | -3 | -40Yes

Methods

MethodParameters
activateNext()none
activatePrevious()none
selectItem()index: number
getSelectedIndices()none

Slots

SlotDescription
(default)

CSS Custom Properties

Override on the host element for per-instance theming:

PropertyDescription
--md-list-container-colorContainer background
--md-list-container-shapeBorder-radius override
--md-list-padding-blockVertical padding inside the list
--md-list-padding-inlineHorizontal padding inside the list
--md-list-segmented-gapGap between rows in segmented mode (default 2px)
--md-list-drop-placeholder-colorDrop-target ghost fill (default primary-container @ ~45%, on-theme)
--md-list-drop-placeholder-shapeDrop-target ghost corner radius (default corner-large / 16px)
--md-list-drop-placeholder-opacityDrop-target ghost opacity (default 1)
--md-list-drop-placeholder-outline-color
--md-list-drop-placeholder-outline-width

CSS Shadow Parts

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

PartDescription
drop-placeholderGhost slot shown in the lifted row's vacated position

Each row is an md-list-item. Its API lives here rather than on a separate page: outside a list it has no list semantics and no roving focus, and inside one the parent writes several of its props for you.

  • Pick one leading visual. leading-icon, leading-avatar and leading-image compete for the same region — combining them is a bug, not a layering feature.
  • lines sets the height contract; it does not truncate. lines="1" with three lines of text overflows. It auto-promotes (an overline implies 2 lines, overline + supporting-text implies 3), so set it explicitly only to override.
  • Listen on the list, not the row. mdActivate / mdSelect are indexed and reconciled; the row’s own mdClick / mdItemClick / mdItemSelect are lower-level plumbing.
  • Parent-managed props — don’t set these yourself: selectionMode, interactionMode, reorderable, rovingFocusVisible, containerRole. The list writes them imperatively.
  • expandable rows own their expansion (mdExpand, toggle(), expand(), collapse()), which is entirely separate from selection.
  • With a selection-mode on the parent, the row becomes focusable and clickable regardless of type.

Both rows below ask for the same three regions. The first uses props, the second uses the matching slots — and the third proves the point: it sets headline and leading-icon and slots them, and only the slotted content survives.

Props, slots, and what happens when you supply both Open in Storybook
Set by slots leading, headline, supporting-text slots The slot wins The headline prop and leading-icon here are dead config
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-list label="Props versus slots" style="inline-size: 400px;">
  <md-list-item type="button" headline="Set by props" supporting-text="headline, supporting-text, leading-icon" leading-icon="tune" trailing-supporting-text="props"></md-list-item>
  <md-divider inset-start></md-divider>
  <md-list-item type="button" lines="2">
    <span slot="leading" class="material-symbols-outlined" aria-hidden="true">tune</span>
    <span slot="headline">Set by <strong>slots</strong></span>
    <span slot="supporting-text">leading, headline, supporting-text</span>
    <span slot="trailing-supporting-text">slots</span>
  </md-list-item>
  <md-divider inset-start></md-divider>
  <md-list-item type="button" lines="2" headline="This prop never renders" leading-icon="close">
    <span slot="leading" class="material-symbols-outlined" aria-hidden="true">check</span>
    <span slot="headline">The slot wins</span>
    <span slot="supporting-text">The headline prop and leading-icon here are dead config</span>
  </md-list-item>
</md-list>

Properties

PropertyAttributeTypeDefaultReflects
typetypeMdListItemType'text'Yes
headlineheadlinestring''
overlineoverlinestring''
supportingTextsupporting-textstring''
trailingSupportingTexttrailing-supporting-textstring''
leadingIconleading-iconstring''
trailingIcontrailing-iconstring''
leadingAvatarleading-avatarstring''
leadingAvatarAltleading-avatar-altstring''
leadingAvatarNameleading-avatar-namestring''
leadingAvatarLabelleading-avatar-labelstring''
leadingImageleading-imagestring''
leadingImageAltleading-image-altstring''
lineslines1 | 2 | 31
hrefhrefstring''
targettargetstring'_self'
disableddisabledbooleanfalseYes
softDisabledsoft-disabledbooleanfalseYes
selectedselectedbooleanfalseYes
expandableexpandablebooleanfalseYes
expandedexpandedbooleanfalseYes
tabbabletabbablebooleantrue
rovingFocusVisibleroving-focus-visiblebooleanfalse
selectionModeselection-mode'none' | 'single-select' | 'multi-select''none'
interactionModeinteraction-mode'single-action' | 'multi-action''single-action'
containerRolecontainer-rolestring''
reorderablereorderablebooleanfalse
densitydensity0 | -1 | -2 | -3 | -40Yes

Methods

MethodParameters
setFocus()options?: FocusOptions
focusItem()none
toggle()none
expand()none
collapse()none

Slots

SlotDescription
(default)
leadingCustom leading content (icon / avatar / image)
overlineRich overline (overrides `overline` prop)
headlineRich headline (overrides `headline` prop)
supporting-textRich supporting text
trailingCustom trailing content (icon button, switch, etc.)
trailing-supporting-textRich trailing supporting text
expanded-contentContent revealed when an expandable row is expanded

CSS Custom Properties

Override on the host element for per-instance theming:

PropertyDescription
--md-list-item-container-colorContainer background (M3 default: surface = #FEF7FF light)
--md-list-item-container-shapeBorder-radius shorthand (all four corners)
--md-list-item-container-shape-start-startTop-start corner (used by md-list grouped style)
--md-list-item-container-shape-start-endTop-end corner (used by md-list grouped style)
--md-list-item-container-shape-end-startBottom-start corner
--md-list-item-container-shape-end-endBottom-end corner
--md-list-item-active-container-shapeMeta-override applied to every active state
--md-list-item-hover-container-shapeBorder-radius on hover (default 0 — no morph; opt-in)
--md-list-item-focus-container-shapeBorder-radius on focus (default 16px / corner-large)
--md-list-item-pressed-container-shapeBorder-radius on press (default 16px / corner-large)
--md-list-item-selected-container-shapeBorder-radius on selected (default 16px / corner-large)
--md-list-item-active-container-shapeShared default for all four states above
--md-list-item-dragged-container-shapeBorder-radius on dragged (default 16px / corner-large)
--md-list-item-hover-state-layer-opacityState-layer opacity on hover (M3: 0.08)
--md-list-item-focus-state-layer-opacityState-layer opacity on focus (M3: 0.10)
--md-list-item-pressed-state-layer-opacityState-layer opacity on press (M3: 0.10)
--md-list-item-dragged-state-layer-opacityState-layer opacity when dragged (M3: 0.16)
--md-list-item-label-text-colorHeadline text color
--md-list-item-label-text-fontHeadline font family
--md-list-item-supporting-text-colorSupporting text color
--md-list-item-supporting-text-fontSupporting text font family
--md-list-item-overline-colorOverline text color
--md-list-item-trailing-supporting-text-colorTrailing supporting text color
--md-list-item-trailing-supporting-text-fontTrailing supporting text font family
--md-list-item-leading-icon-colorLeading icon color
--md-list-item-leading-icon-sizeLeading icon size, default (M3: 24px)
--md-list-item-trailing-icon-colorTrailing icon color
--md-list-item-unselected-trailing-icon-colorTrailing icon color when row is in selection
--md-list-item-trailing-icon-sizeTrailing icon size, default (M3: 24px)
--md-list-item-trailing-icon-expressive-sizeTrailing icon size, Expressive list-style (M3: 20px)
--md-list-item-leading-spaceInline-start padding (M3: 16px)
--md-list-item-trailing-spaceInline-end padding (M3: 16px)
--md-list-item-top-spaceBlock-start padding (M3: 8px)
--md-list-item-bottom-spaceBlock-end padding (M3: 10px)
--md-list-item-between-spaceGap between leading / content / trailing (M3: 12px)
--md-list-item-three-line-top-spaceBlock-start padding when 3-line / 88px+ (M3: 12px)
--md-list-item-three-line-bottom-spaceBlock-end padding when 3-line / 88px+ (M3: 12px)
--md-list-item-dragged-container-elevationBox-shadow applied when host carries
--md-list-item-leading-video-widthWidth of <video slot="leading"> thumbnail (M3: 100px)
--md-list-item-leading-video-heightHeight of <video slot="leading"> thumbnail (M3: 56px)
--md-list-item-leading-video-shapeBorder-radius of <video slot="leading"> (M3: corner-small)
--md-list-item-leading-video-top-spaceBlock-start padding bumped when the row carries
--md-list-item-leading-avatar-colorAvatar circle color (label avatar)
--md-list-item-leading-avatar-label-colorAvatar label text color
--md-list-item-leading-avatar-sizeAvatar dimensions (default 40px)
--md-list-item-leading-avatar-shapeAvatar border-radius (default full)
--md-list-item-leading-image-widthImage thumbnail width (default 56px)
--md-list-item-leading-image-heightImage thumbnail height (default 56px)
--md-list-item-leading-image-shapeImage thumbnail radius, default (M3: 0)
--md-list-item-leading-image-expressive-shapeImage thumbnail radius, Expressive list-style
--md-list-item-leading-image-selection-scrim-colorFull-thumbnail scrim when selected (M3: scrim @ 32%)
--md-list-item-leading-image-selection-circle-colorCheckmark circle fill when selected (M3: on-surface @ 60%)
--md-list-item-leading-image-selection-icon-colorCheckmark glyph color (M3: surface / white)
--md-list-item-leading-image-selection-icon-sizeCheckmark circle diameter (M3: 24px)
--md-list-item-selected-container-colorSelected container background (M3: secondary-container)
--md-list-item-selected-label-text-colorSelected headline color (M3: on-secondary-container)
--md-list-item-selected-active-icon-colorLeading/trailing icon color when row is BOTH
--md-list-item-selected-disabled-container-colorContainer fill when row is BOTH selected AND
--md-list-item-expand-chevron-colorCaret icon color when collapsed (forwarded to the
--md-list-item-expand-durationExpanded-panel open duration (M3: medium3 / 350ms)
--md-list-item-collapse-durationExpanded-panel close duration (M3: short4 / 200ms)
--md-list-item-expanded-panel-padding-blockExpanded panel vertical padding
--md-list-item-expanded-panel-padding-inlineExpanded panel horizontal padding (both sides)
--md-list-item-expanded-panel-padding-inline-startExpanded panel start padding —
--md-list-item-expanded-panel-backgroundExpanded panel background
--md-list-item-leading-icon-expressive-size
--md-list-item-disabled-state-layer-opacity
--md-list-item-expand-button-size
--md-list-item-expand-chevron-size

CSS Shadow Parts

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

PartDescription
leadingLeading slot container
avatarLeading avatar (image or label)
image-wrapWrapper around prop-based leading image + selection overlay
imageLeading image thumbnail
image-selection-indicatorSelected-state scrim overlay on image thumbnail
image-selection-circleCheckmark circle fill centered on thumbnail
image-selection-checkMaterial Symbols check glyph inside the circle
leading-iconLeading Material Symbols icon
contentContent slot container (overline + headline + supporting)
overlineOverline text element
headlineHeadline text element
supporting-textSupporting text element
trailingTrailing slot container
trailing-supporting-textTrailing supporting text element
trailing-iconTrailing Material Symbols icon
expand-buttonTrailing caret md-icon-button on expandable rows
drag-handleTrailing drag grip on reorderable rows
primary
rowRow chassis (leading + content + trailing)
state-layerState-layer overlay (interactive rows only)
expanded-panelWrapper revealed when `expanded` is true
expanded-panel-innerInner wrapper that holds the slot
avatar-image
avatar-initials
avatar-icon
headline-tooltip-popupPopup of the headline overflow tooltip (forwarded)
supporting-tooltip-popupPopup of the supporting-text overflow tooltip (forwarded)
expand-button-state-layerState-layer of the caret button (forwarded)
expand-button-iconIcon glyph wrapper of the caret button (forwarded;
  • Name the list. label or labelledby — an unnamed list of rows is hard to navigate with a screen reader, and M3 asks for it explicitly.
  • The list manages roving focus: the whole list is one tab stop and arrow keys move between rows.
  • With multi-action rows, every embedded control needs its own accessible name identifying which row it belongs to.
  • Selection state is exposed by the items (aria-selected), not by the list wrapper; multi-select adds aria-multiselectable to the list.
  • Reordering has a keyboard path built inAlt + ArrowUp / Alt + ArrowDown on the focused row — so a reorderable list is not pointer-only. It is silent, though: announce the move yourself from mdReorder if the reorder is a primary task.
  • role-override exists for cases where the list is really a listbox or menu in context. Leave it empty unless you know you need it.
  • headline is the row’s primary accessible text. Decorative leading visuals should be hidden from assistive tech — a leading-avatar beside the same name in the headline is redundant, and leading-image-alt should be empty for a purely decorative thumbnail.
  • On a row, disabled leaves the tab order while soft-disabled stays focusable, so the row remains discoverable.
  • A directional trailing-icon such as chevron_right must be swapped by you under RTL; everything else mirrors automatically.

Tab into the list below: it takes one tab stop, not four. Arrow keys move between rows, Home / End jump to the ends, and typing a letter jumps to the first row whose headline starts with it. Because these rows are multi-action, Tab from a row reaches its own trailing button — and each of those buttons names its row, so a screen reader announces “Archive report.pdf”, never a bare “Archive”.

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

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

<md-list interaction-mode="multi-action" label="Documents" style="inline-size: 380px;">
  <md-list-item type="button" headline="report.pdf" supporting-text="Focusable — press Enter to activate" leading-icon="picture_as_pdf">
    <md-icon-button slot="trailing" icon="archive" aria-label="Archive report.pdf"></md-icon-button>
  </md-list-item>
  <md-divider inset-start></md-divider>
  <md-list-item type="text" headline="budget.xlsx" supporting-text="A text row — skipped by roving focus" leading-icon="table_chart"></md-list-item>
  <md-divider inset-start></md-divider>
  <md-list-item type="button" headline="Sealed contract" supporting-text="Soft-disabled — still reachable, still announced" leading-icon="lock" soft-disabled>
    <md-icon-button slot="trailing" icon="archive" aria-label="Archive sealed contract" disabled></md-icon-button>
  </md-list-item>
  <md-divider inset-start></md-divider>
  <md-list-item type="button" headline="notes.txt" supporting-text="Type n to jump straight here" leading-icon="description">
    <md-icon-button slot="trailing" icon="archive" aria-label="Archive notes.txt"></md-icon-button>
  </md-list-item>
</md-list>

The type="text" row is the one to notice: with no selection mode on the list it is not focusable, so arrow keys step straight past it. Give the list a selection-mode and the same row becomes focusable, because picking from a list of label-only rows is the whole point of a selection list.

RTL — leading and trailing sides, row padding and the drag handle all mirror automatically under dir="rtl". Nothing on the component opts in; the same markup is simply laid out the other way round:

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

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

<div style="display:grid;grid-template-columns:auto 1fr;gap:14px 16px;align-items:center">
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">ltr</span>
  <div dir="ltr">
    <md-list label="Settings" style="max-inline-size:320px;">
      <md-list-item headline="Wi-Fi" supporting-text="Home network" leading-icon="wifi" trailing-supporting-text="On"></md-list-item>
      <md-list-item headline="Bluetooth" supporting-text="2 devices" leading-icon="bluetooth" trailing-supporting-text="On"></md-list-item>
    </md-list>
  </div>
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">rtl</span>
  <div dir="rtl">
    <md-list label="Settings" style="max-inline-size:320px;">
      <md-list-item headline="Wi-Fi" supporting-text="Home network" leading-icon="wifi" trailing-supporting-text="On"></md-list-item>
      <md-list-item headline="Bluetooth" supporting-text="2 devices" leading-icon="bluetooth" trailing-supporting-text="On"></md-list-item>
    </md-list>
  </div>
</div>

A directional trailing-icon is yours to swap

Section titled “A directional trailing-icon is yours to swap”

The layout mirrors; the glyph inside trailing-icon does not. A chevron_right that meant “forward” in LTR still points right in RTL, where forward is left. Both lists below are dir="rtl" and differ only in that one attribute:

trailing-icon under dir=rtl — chevron_right is wrong, chevron_left is right
right
left
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;">right</span>
  <div dir="rtl">
    <md-list label="جهات الاتصال — خطأ" style="max-inline-size:360px;">
      <md-list-item headline="آدا لوفلايس" supporting-text="مهندسة" leading-icon="person" trailing-icon="chevron_right"></md-list-item>
      <md-list-item headline="آلان تورنغ" supporting-text="عالم رياضيات" leading-icon="person" trailing-icon="chevron_right"></md-list-item>
    </md-list>
  </div>
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">left</span>
  <div dir="rtl">
    <md-list label="جهات الاتصال" style="max-inline-size:360px;">
      <md-list-item headline="آدا لوفلايس" supporting-text="مهندسة" leading-icon="person" trailing-icon="chevron_left"></md-list-item>
      <md-list-item headline="آلان تورنغ" supporting-text="عالم رياضيات" leading-icon="person" trailing-icon="chevron_left"></md-list-item>
    </md-list>
  </div>
</div>

density="-1…-4" on the list drives the same --md-sys-density-scale signal a global data-density ancestor sets, so a value here simply overrides the inherited one — and it cascades to every row.

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

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

<div style="display:grid;grid-template-columns:auto 1fr;gap:14px 16px;align-items:center">
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">0</span>
  <md-list label="Rung 0" density="0" style="inline-size:280px;">
    <md-list-item headline="Ada Lovelace" supporting-text="Engineer" leading-icon="person"></md-list-item>
    <md-list-item headline="Alan Turing" supporting-text="Mathematician" leading-icon="person"></md-list-item>
  </md-list>
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">-1</span>
  <md-list label="Rung -1" density="-1" style="inline-size:280px;">
    <md-list-item headline="Ada Lovelace" supporting-text="Engineer" leading-icon="person"></md-list-item>
    <md-list-item headline="Alan Turing" supporting-text="Mathematician" leading-icon="person"></md-list-item>
  </md-list>
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">-2</span>
  <md-list label="Rung -2" density="-2" style="inline-size:280px;">
    <md-list-item headline="Ada Lovelace" supporting-text="Engineer" leading-icon="person"></md-list-item>
    <md-list-item headline="Alan Turing" supporting-text="Mathematician" leading-icon="person"></md-list-item>
  </md-list>
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">-3</span>
  <md-list label="Rung -3" density="-3" style="inline-size:280px;">
    <md-list-item headline="Ada Lovelace" supporting-text="Engineer" leading-icon="person"></md-list-item>
    <md-list-item headline="Alan Turing" supporting-text="Mathematician" leading-icon="person"></md-list-item>
  </md-list>
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">-4</span>
  <md-list label="Rung -4" density="-4" style="inline-size:280px;">
    <md-list-item headline="Ada Lovelace" supporting-text="Engineer" leading-icon="person"></md-list-item>
    <md-list-item headline="Alan Turing" supporting-text="Mathematician" leading-icon="person"></md-list-item>
  </md-list>
</div>

They are independent signals, so they compose. Here the wrapper is dir="rtl" with a global data-density="-2"; the first list simply inherits that rung, and the second declares density="-4" on itself, which wins over the inherited value:

dir=rtl with data-density=-2, and one list overriding the rung to -4
-2 -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 dir="rtl" data-density="-2" 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;">-2</span>
  <md-list label="مضغوط" style="max-inline-size:320px;">
    <md-list-item headline="آدا لوفلايس" supporting-text="مهندسة" leading-icon="person" trailing-icon="chevron_left"></md-list-item>
    <md-list-item headline="آلان تورنغ" supporting-text="عالم رياضيات" leading-icon="person" trailing-icon="chevron_left"></md-list-item>
  </md-list>
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">-4</span>
  <md-list label="مضغوط جدا" density="-4" style="max-inline-size:320px;">
    <md-list-item headline="آدا لوفلايس" supporting-text="مهندسة" leading-icon="person" trailing-icon="chevron_left"></md-list-item>
    <md-list-item headline="آلان تورنغ" supporting-text="عالم رياضيات" leading-icon="person" trailing-icon="chevron_left"></md-list-item>
  </md-list>
</div>

Density — set it on the list, not on the rows. The rung is declared on the list’s host as --md-sys-density-scale, which every row inherits, so one attribute keeps the whole stack in step. See Density and RTL.

i18n — translate label and all row content. Longer translations increase row height, and M3’s line-length guidance matters more in verbose languages — adjust the list’s margins rather than letting lines run.

Custom propertyPurposeDefault
--md-list-container-colorList surfacetransparent for both list styles — opt in to a painted chassis
--md-list-container-shapeList corner radius0
--md-list-padding-block / --md-list-padding-inlineList padding8px / 0
--md-list-segmented-gapGap between tiles in segmented style2px
--md-list-drop-placeholder-colorDrag placeholder fillprimary-container at 45%
--md-list-drop-placeholder-shapeDrag placeholder radiuscorner-large (16px)
--md-list-drop-placeholder-opacityDrag placeholder opacity1
--md-list-drop-placeholder-outline-color / -widthDrag placeholder borderprimary at 55% / 2px
Themed instance Open in Storybook
Show code for each technology
<!-- index.html <head> — the icon font the components draw from -->
<link rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">

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

<md-list list-style="segmented" label="Themed list" style="inline-size: 300px; --md-list-container-color: var(--md-sys-color-surface-container-highest); --md-list-container-shape: 24px; --md-list-segmented-gap: 8px; --md-list-padding-inline: 12px; --md-list-padding-block: 12px;">
  <md-list-item type="button" headline="Photos" leading-icon="photo"></md-list-item>
  <md-list-item type="button" headline="Videos" leading-icon="videocam"></md-list-item>
  <md-list-item type="button" headline="Music" leading-icon="music_note"></md-list-item>
</md-list>

A segmented list zeroes both paddings by default, because its tiles are meant to float directly on the page surface with no container behind them. The moment you tint one with --md-list-container-color, set --md-list-padding-block and --md-list-padding-inline yourself — otherwise the rows sit flush against the container edges, which reads as a rendering bug rather than a design.

Check any override in both themes before you ship it.

Row appearance is driven by the --md-list-item-* properties. The full set is in the row API reference; these are the ones reached for most, and all of them can be set on the list, because the rows inherit them:

Custom propertyPurpose
--md-list-item-container-color / --md-list-item-container-shapeRow surface
--md-list-item-container-shape-start-start-end-endPer-corner radii
--md-list-item-active-container-shape, or -hover- / -focus- / -pressed- / -selected- / -dragged- individuallyPer-state shape
--md-list-item-hover-state-layer-opacity / -focus- / -pressed-State layer
--md-list-item-expand-duration / --md-list-item-collapse-durationExpandable motion

The per-corner properties are how you round the ends of a group. They have to go on the individual rows, not on the list: a value set on the list is inherited by every row equally, so it cannot single out the first and the last.

Per-corner shape on the first and last row only
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-list label="Rounded group ends" style="inline-size: 300px; --md-list-item-container-color: var(--md-sys-color-surface-container-high);">
  <md-list-item type="button" headline="First" leading-icon="photo" style="--md-list-item-container-shape-start-start: 20px; --md-list-item-container-shape-start-end: 20px;"></md-list-item>
  <md-list-item type="button" headline="Middle" leading-icon="videocam"></md-list-item>
  <md-list-item type="button" headline="Last" leading-icon="music_note" style="--md-list-item-container-shape-end-start: 20px; --md-list-item-container-shape-end-end: 20px;"></md-list-item>
</md-list>

CSS partdrop-placeholder, the ghost slot shown while a row is dragged (the same affordance md-accordion uses).

::part(drop-placeholder) — drag a row to see it Open in Storybook
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<style>
  .list-parts::part(drop-placeholder) {
  border-style: solid;
  border-width: 3px;
  border-color: var(--md-sys-color-tertiary);
  background: color-mix(in srgb, var(--md-sys-color-tertiary) 20%, transparent);
  }
</style>
<md-list class="list-parts" label="Styled drop target" reorderable style="inline-size: 300px;">
  <md-list-item headline="Drag me" supporting-text="The ghost slot is restyled"></md-list-item>
  <md-list-item headline="Second row"></md-list-item>
  <md-list-item headline="Third row"></md-list-item>
</md-list>

md-table · md-card · md-menu · md-accordion · md-transfer-list · md-divider

For AI Agents — md-list

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

# md-list

<!-- llm:meta
tag: md-list
category: containment
status: md3-mapped
m3-guidelines: https://m3.material.io/components/lists/guidelines
form-associated: false
depends-on: none
used-by: none
accepts-children: md-list-item
-->

**A vertical set of related rows.** It owns the container role and selection.
Roving focus, arrow-key and typeahead navigation, and optional drag-to-reorder
are all coordinated here on behalf of its `md-list-item` children.

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

---

## When to use

- A **vertical collection of similar records**: contacts, files, messages,
  settings.
- Rows that may be selectable, activatable, or reorderable.
- Content that reads top-to-bottom rather than comparing across columns.

## When NOT to use

| Situation | Use instead |
|---|---|
| Rich, self-contained items with their own actions | `md-card` collection |
| A contextual popup of actions | `md-menu` |
| Options in a picker | `md-select` / `md-multi-select` |
| Collapsible content sections | `md-accordion` |
| Navigation destinations | `md-navigation-rail` / `md-navigation-bar` |
| Moving items between two pools | `md-transfer-list` |
| Column-comparable data | `md-table` |

## Decision cues

| Need | Setting |
|---|---|
| One connected chassis, square middle rows | `list-style="standard"` (default) |
| Individually rounded tiles with a gap | `list-style="segmented"` |
| Rows select one at a time | `selection-mode="single-select"` |
| Rows multi-select | `selection-mode="multi-select"` |
| Rows just activate (navigate/open) | `selection-mode="none"` (default) + `type` on the rows |
| A row has several distinct controls | `interaction-mode="multi-action"` |
| Drag to reorder | `reorderable` |
| Name the list | `label`, or `labelledby` pointing at a heading's id |
| Wire the list into a menu pattern | `role-override="menu"` |
| Hairlines between rows | Interleave `<md-divider>` children |

## API contract

```html
<md-list
  list-style="standard|segmented"                    <!-- default: standard -->
  selection-mode="none|single-select|multi-select"   <!-- default: none -->
  interaction-mode="single-action|multi-action"      <!-- default: single-action -->
  reorderable                                        <!-- default: false -->
  label="Contacts"                                   <!-- default: "" -->
  labelledby="contacts-heading"                      <!-- default: "" -->
  role-override=""                                   <!-- default: "" -->
  density="-1|-2|-3|-4"                              <!-- default: 0 (uncompacted) -->
>
  <md-list-item headline="Ada Lovelace"></md-list-item>
  <md-list-item headline="Grace Hopper"></md-list-item>
</md-list>
```

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

| Event | Detail | Fires |
|---|---|---|
| `mdSelect` | `{ index, value, selected, item, childIndex?, expanded? }` | Selection changed **through user input** (click / Enter / Space). `value` is the row's `headline`, falling back to its text content. `childIndex` / `expanded` are present only when the selected row lives in an expandable parent's `expanded-content`. |
| `mdActivate` | `{ index, item }` | The active (roved) row **changed** — click, arrow key, `Home`/`End`, typeahead. Not an "open this" signal. |
| `mdReorder` | `{ from, to, order }` | A drag or `Alt+Arrow` reorder completed. `order[newPosition] === originalPosition` against the as-authored order. |

**Methods**

| Method | Returns | Notes |
|---|---|---|
| `activateNext()` | `Promise<HTMLMdListItemElement \| null>` | Roving focus forward, wraps at the end |
| `activatePrevious()` | `Promise<HTMLMdListItemElement \| null>` | Roving focus back, wraps at the start |
| `selectItem(index)` | `Promise<void>` | No-op when `selection-mode="none"`; **does not emit `mdSelect`** |
| `getSelectedIndices()` | `Promise<number[]>` | Indices of rows whose `selected` is true |

**Slots** — `(default)`: `md-list-item` children, optionally interleaved with
`md-divider`.

**Parts** — `drop-placeholder` (the ghost slot shown under the insertion point
during a pointer reorder; only in the DOM while dragging).

### Behavioral contract worth knowing

- **Only direct `md-list-item` children count.** Rows are found with
  `children.filter(el => el.tagName === 'MD-LIST-ITEM')`, so wrapping rows in a
  `<div>` for layout hides them from indexing, roving focus and reordering. A
  `MutationObserver` re-syncs when children are added or removed.
- **`mdActivate` is a focus/roving signal, not an "open this" signal.** It
  fires whenever the active row *changes*; clicking the row that is already
  active emits nothing. Navigate from the row's own `type="link"`/`type="button"`
  behaviour, or from `mdSelect`, not from this.
- **Selection and activation are different.** `mdSelect` means "the selected set
  changed"; a list can do one, both, or neither.
- **The list writes props onto its children** at load and on every child-list
  mutation: `selectionMode`, `interactionMode`, `reorderable`, and
  `containerRole`. Don't set those on `md-list-item` yourself — they will be
  overwritten. `density` is *not* pushed as a prop; it cascades as the
  `--md-sys-density-scale` custom property.
- **The container role is derived**, in this order: `role-override` if set →
  `listbox` when a selection mode is on → but back to `list` if any direct row
  is `expandable` (those rows own nested option semantics instead) → otherwise
  `list`. `aria-multiselectable="true"` is added only when the role really
  resolves to `listbox`.
- **Interleaved `md-divider`s are hidden from AT automatically** when the role
  is `list` or `listbox`, because those containers may not own a `separator`.
  Under a `role-override` such as `menu` they keep their separator semantics.
- **Keyboard**, handled on the list: `ArrowDown`/`ArrowUp` move roving focus and
  wrap; `Home`/`End` jump to the ends; any printable character starts a
  typeahead that matches the prefix of a row's `headline` (or text content) and
  clears after 500 ms of inactivity; `Alt+ArrowUp`/`Alt+ArrowDown` reorder when
  `reorderable`.
- The list **stops handling arrow keys entirely** when it is inside an
  `md-search`, which owns that navigation.
- With `selection-mode="none"`, focusable rows are those whose `type` is not
  `text`. Turning on a selection mode (or `reorderable`) makes **every**
  non-disabled row focusable, including plain `type="text"` rows.
- `single-select` clears the other rows' `selected` before setting the new one;
  `multi-select` toggles the clicked row only.
- `selectItem(index)` changes state **without** emitting `mdSelect` — use it to
  mirror external state, and don't wait for an event you won't get.
- `reorderable` moves the rows in the DOM and emits `mdReorder`; **nothing is
  persisted.** Apply `detail.order` to your data. Disabled rows can't be picked
  up.

---

## Do / Don't

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

| ✅ Do | ❌ Don't |
|---|---|
| Place supporting visuals (thumbnails, avatars) at the **leading** edge to aid scanning | Avoid visuals in the centre of a row — it makes the list hard to scan |
| Use segmented gaps and filled items to define a list group | Don't over-divide a contained list |
| Limit dividers to **uncontained** lists | Don't put a divider between every row of a segmented list |
| Adjust margins for a comfortable line length | Don't scale the list without adjusting text length — long lines hurt readability |
| Use a multi-column layout to break up content when it helps | Don't force a single narrow column on a wide screen |
| Keep rows structurally consistent | Don't mix wildly different row shapes in one list |
| Name the list | Don't ship an unnamed list |

---

## Patterns

```html
<!-- Simple activatable list -->
<md-list label="Contacts">
  <md-list-item type="button" headline="Ada Lovelace" supporting-text="Engineering"></md-list-item>
  <md-list-item type="button" headline="Grace Hopper" supporting-text="Research"></md-list-item>
</md-list>

<script type="module">
  const list = document.querySelector('md-list');
  list.addEventListener('mdActivate', (e) => highlight(e.detail.index));
</script>
```

```html
<!-- Multi-select with trailing controls -->
<md-list id="files" label="Files"
         selection-mode="multi-select" interaction-mode="multi-action">
  <md-list-item headline="report.pdf">
    <md-icon-button slot="trailing" icon="more_vert"
                    aria-label="More actions for report.pdf"></md-icon-button>
  </md-list-item>
  <md-list-item headline="notes.txt">
    <md-icon-button slot="trailing" icon="more_vert"
                    aria-label="More actions for notes.txt"></md-icon-button>
  </md-list-item>
</md-list>

<script type="module">
  const list = document.getElementById('files');
  list.addEventListener('mdSelect', async () => {
    console.log(await list.getSelectedIndices());
  });
</script>
```

```html
<!-- Reorderable: the list moves the DOM, you persist the order -->
<md-list id="playlist" label="Playlist" reorderable>
  <md-list-item headline="Track one"></md-list-item>
  <md-list-item headline="Track two"></md-list-item>
  <md-list-item headline="Track three"></md-list-item>
</md-list>

<script type="module">
  document.getElementById('playlist').addEventListener('mdReorder', (e) => {
    // e.detail.order[newPosition] === originalPosition
    savePlaylist(e.detail.order);
  });
</script>
```

```html
<!-- Segmented tiles, and hairlines via interleaved dividers -->
<md-list list-style="segmented" label="Quick actions">
  <md-list-item type="button" headline="Share" leading-icon="share"></md-list-item>
  <md-list-item type="button" headline="Archive" leading-icon="archive"></md-list-item>
</md-list>

<md-list list-style="standard" label="Settings">
  <md-list-item type="button" headline="Notifications"></md-list-item>
  <md-divider inset></md-divider>
  <md-list-item type="button" headline="Privacy"></md-list-item>
</md-list>
```

```html
<!-- Named by a visible heading -->
<h2 id="contacts-heading">Contacts</h2>
<md-list labelledby="contacts-heading">
  <md-list-item type="button" headline="Ada Lovelace"></md-list-item>
</md-list>
```

## Anti-patterns

| ❌ Wrong | ✅ Right | Why |
|---|---|---|
| Setting `selection-mode` / `interaction-mode` / `reorderable` on `md-list-item` | Set them on `md-list` | The list overwrites them on load and on every child mutation. |
| Wrapping rows in `<div>`s for layout | Keep `md-list-item` as direct children | Row discovery filters direct children by tag name. |
| Waiting for `mdSelect` after calling `selectItem()` | Update your state directly | The method deliberately does not emit. |
| Treating `mdActivate` as "the user opened this row" | Use the row's `type="link"`/`type="button"`, or `mdSelect` | It only reports a roving-focus change. |
| Expecting `reorderable` to persist order | Save `e.detail.order` | The list only moves the DOM and reports. |
| Trailing buttons with `interaction-mode="single-action"` | Use `multi-action` | The row treats the whole body as one target otherwise. |
| A list with no `label`/`labelledby` | Name it | Screen-reader navigation depends on it. |
| `aria-multiselectable` set by hand | Use `selection-mode="multi-select"` | The list writes it, and only when the role really is `listbox`. |
| `role="separator"` expected from interleaved dividers | Accept that they are `aria-hidden` in a `list`/`listbox` | A list may not own a separator. |
| Using a list for column-comparable data | `md-table` | Wrong structure. |
| Dividers between every segmented row | Reserve dividers for uncontained lists | M3 explicit rule. |
| Centre-aligned thumbnails | Leading edge | M3 explicit rule. |

## Accessibility, RTL, density, i18n

**Accessibility**
- Give the list an accessible name (`label`, or `labelledby` pointing at a
  visible heading). It is the single most common defect here.
- The list manages roving focus: one tab stop for the whole list, arrows to
  move within it, `Home`/`End` to jump, typeahead to seek.
- With `multi-action` rows, every embedded control needs a name that identifies
  **which** row it belongs to ("More actions for report.pdf", not "More
  actions").
- Drag-to-reorder has a built-in keyboard equivalent: `Alt+ArrowUp` /
  `Alt+ArrowDown` on the focused row.
- Selection state is exposed by the rows (`aria-selected` on `role="option"`),
  not by the container.
- Reach for `role-override` only when you are deliberately wiring a different
  ARIA pattern (e.g. `menu`); the rows adapt their own roles to match.

**RTL** — leading/trailing regions and padding are logical and mirror under
`dir="rtl"`. Reordering is vertical only, so it is unaffected.

**Density** — `density="-1…-4"` on the list sets `--md-sys-density-scale`,
which inherits into the rows, so one value compacts the whole list. Rung `0` is
the uncompacted default and has no rule of its own; to pin one row back to full
size, set `style="--md-sys-density-scale: 0"` on it. Verify touch targets at
deep rungs.

**i18n** — translate `label` and all row content. Longer translations increase
row height; M3's line-length guidance matters more in verbose languages.

## Related components

`md-list-item` · `md-divider` · `md-table` · `md-card` · `md-menu` ·
`md-accordion` · `md-transfer-list`

## Theming

| Custom property | Purpose | Default |
|---|---|---|
| `--md-list-container-color` | List chassis background | `transparent` (both `standard` and `segmented`) |
| `--md-list-container-shape` | Corner radius of the list chassis | `0px` |
| `--md-list-padding-block` | Block padding | `--md-sys-spacing-inset-sm` (`8px`) in `standard`; `0px` in `segmented` |
| `--md-list-padding-inline` | Inline padding | `0px` |
| `--md-list-segmented-gap` | Gap between tiles in `list-style="segmented"` | `2px` |
| `--md-list-drop-placeholder-color` | Drop-target fill | 45% `--md-sys-color-primary-container` |
| `--md-list-drop-placeholder-outline-color` | Drop-target outline | 55% `--md-sys-color-primary` |
| `--md-list-drop-placeholder-outline-width` | Drop-target outline width | `2px` |
| `--md-list-drop-placeholder-shape` | Drop-target radius | `--md-sys-shape-corner-large` (`16px`) |
| `--md-list-drop-placeholder-opacity` | Drop-target opacity | `1` |

**CSS parts** — `drop-placeholder`.

Row appearance is themed separately with the `--md-list-item-*` properties —
see the `md-list-item` readme.

```css
md-list.card {
  --md-list-container-color: var(--md-sys-color-surface-container);
  --md-list-container-shape: 16px;
  --md-list-padding-inline: 8px;
}
```

<!-- Auto Generated Below -->


## Properties

| Property          | Attribute          | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | Type                                          | Default           |
| ----------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- | ----------------- |
| `density`         | `density`          | Local density rung. Drives the same `--md-sys-density-scale` signal that a global `data-density` ancestor sets, so a local value simply overrides the inherited one. 0 = default, -4 = ultra-compact.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | `-1 \| -2 \| -3 \| -4 \| 0`                   | `0`               |
| `interactionMode` | `interaction-mode` | Row interaction pattern for the whole list. A list uses exactly one interaction mode at a time (per the M3 Lists spec).  - `single-action` (default): each row is one tappable area — leading   icon + label activate together. Use `type="button"` (or selection   mode) on items. - `multi-action`: rows expose a primary action (the row body) plus   optional secondary actions in `slot="trailing"` (e.g. `md-icon-button`).   Clicks on trailing controls do not activate the primary row action;   each secondary control keeps its own focus stop and keyboard behavior.                                                                                                                                                                                                                                          | `"multi-action" \| "single-action"`           | `'single-action'` |
| `label`           | `label`            | Optional aria-label for the container.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    | `string`                                      | `''`              |
| `labelledby`      | `labelledby`       | Optional aria-labelledby for the container.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | `string`                                      | `''`              |
| `listStyle`       | `list-style`       | Visual style of the list.  - `standard` (default, M3 Expressive): rows share a single rounded   chassis with **no gap** between them. The first row rounds its   top corners, the last rounds its bottom corners, and middle rows   are square so the stack reads as one continuous shape — the M3   "connected" pattern. Best for settings panels, option groups,   and iOS-style sectioned tables. To add hairline separators   between rows, interleave `<md-divider>` elements. - `segmented` (M3 Expressive): rows are individually rounded "tiles"   separated by a small gap. Each tile rounds all four corners. Best   for action sets, quick-action menus, and card-like sections where   each row should read as its own affordance.                                                                            | `"segmented" \| "standard"`                   | `'standard'`      |
| `reorderable`     | `reorderable`      | Enable drag-to-reorder. Each row renders a trailing drag handle (grab it to drag the row to a new position); rows can also be moved with `Alt + ArrowUp / ArrowDown` while focused. On drop the list reorders its children in the DOM and emits {@link mdReorder}. Disabled rows can't be picked up. Works alongside `single-action` / `multi-action` and selection modes.                                                                                                                                                                                                                                                                                                                                                                                                                                                | `boolean`                                     | `false`           |
| `roleOverride`    | `role-override`    | Override the auto-computed ARIA role. Useful when wiring the list into a wider widget pattern (e.g. `role="menu"` for a dropdown). Leave empty to use the role implied by `selectionMode`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | `string`                                      | `''`              |
| `selectionMode`   | `selection-mode`   | Selection coordination across child rows.  - `none` (default): each row stands alone and the container is always   `role="list"` (rows are `listitem`s). Interactive rows (`type="button"` /   `type="link"`) expose their widget role on an inner primary element, so   the list validly owns only `listitem`s while still providing the same   roving-tabindex + arrow-key navigation. `type="text"` rows stay   non-focusable. - `single-select`: the container behaves as `role="listbox"`, items   become `role="option"`, and exactly one item carries `aria-selected`. - `multi-select`: same as above plus `aria-multiselectable="true"`.  Switching to a selection mode also auto-promotes plain `type="text"` rows to focusable so users can pick from a list of label-only rows without authoring extra props. | `"multi-select" \| "none" \| "single-select"` | `'none'`          |


## Events

| Event        | Description                                                                                                                                                                                                                                                                                                                                               | Type                                                                                                                                                                |
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `mdActivate` | Emitted whenever roving focus lands on a different row.                                                                                                                                                                                                                                                                                                   | `CustomEvent<{ index: number; item: HTMLMdListItemElement; }>`                                                                                                      |
| `mdReorder`  | Emitted after a drag (or keyboard) reorder completes.  - `from` / `to`: the moved row's old and new positions (item indices,   ignoring non-item children like dividers). - `order`: the new order expressed as the original (as-authored) indices,   so `order[newPosition] === originalPosition`. Use it to reorder a   backing data array in one pass. | `CustomEvent<{ from: number; to: number; order: number[]; }>`                                                                                                       |
| `mdSelect`   | Emitted when selection changes via user input (click / Enter / Space).                                                                                                                                                                                                                                                                                    | `CustomEvent<{ index: number; value: string; selected: boolean; item: HTMLMdListItemElement; childIndex?: number \| undefined; expanded?: boolean \| undefined; }>` |


## Methods

### `activateNext() => Promise<HTMLMdListItemElement | null>`



#### Returns

Type: `Promise<HTMLMdListItemElement | null>`



### `activatePrevious() => Promise<HTMLMdListItemElement | null>`

Move roving focus to the previous focusable item, wrapping at the start.
Returns the newly active item, or `null` when no items are focusable.

#### Returns

Type: `Promise<HTMLMdListItemElement | null>`



### `getSelectedIndices() => Promise<number[]>`

Returns the indices of all currently selected items.

#### Returns

Type: `Promise<number[]>`



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

Programmatically toggle selection on the item at `index` according
to the current `selection-mode`. Does not emit `mdSelect` (use it
to sync external state without a user action).

#### Parameters

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

#### Returns

Type: `Promise<void>`




## Shadow Parts

| Part                 | Description |
| -------------------- | ----------- |
| `"drop-placeholder"` |             |


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

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