Skip to content

Accordion

Progressive disclosure for a set of sections. Groups md-accordion-item children, coordinates which are expanded, sets the heading level and the panel landmark policy, and adds optional drag-reordering and a floating “chassis” mode.

Live preview Open in Storybook

Standard delivery is free on orders over 50.

Unused items can be returned within 30 days.

Cards, bank transfer and store credit.

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-accordion exclusive heading-level="2" default-expanded="0" style="--md-accordion-gap: var(--md-sys-spacing-gap-lg, 16px);">
  <md-accordion-item headline="Shipping" supporting-text="Arrives in 3-5 days" icon="local_shipping">
    <p>Standard delivery is free on orders over 50.</p>
  </md-accordion-item>
  <md-accordion-item headline="Returns" icon="undo">
    <p>Unused items can be returned within 30 days.</p>
  </md-accordion-item>
  <md-accordion-item headline="Payment" icon="credit_card">
    <p>Cards, bank transfer and store credit.</p>
  </md-accordion-item>
</md-accordion>

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


<md-accordion></md-accordion>
  • Several independent sections where the user only needs one or two at a time: FAQs, settings groups, filter categories.
  • Long content that would overwhelm the page fully expanded.
  • Sections the user may want to reorder (reorderable).
SituationUse instead
Switching between sibling viewsmd-tabs
A linear, ordered processmd-stepper
A list of recordsmd-list
Content users need to compare side by sideShow it all, or md-tabs
A single collapsible regionOne md-accordion-item, or a plain details element
Navigationmd-navigation-rail / md-navigation-bar
Hiding critical informationNothing — keep it visible

Both variants share the same expand/collapse mechanics; only the surface differs.

VariantSurface
filledDefault. Each item is its own tonal tile, separated by --md-accordion-gap
outlinedItems share one rounded chassis with an outline and internal dividers
filled and outlined
Tonal tiles. Each with its own surface. A shared chassis. Divided, not separated.
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-accordion variant="filled" default-expanded="0" style="--md-accordion-gap: var(--md-sys-spacing-gap-lg, 16px);">
  <md-accordion-item headline="Filled — one">Tonal tiles.</md-accordion-item>
  <md-accordion-item headline="Filled — two">Each with its own surface.</md-accordion-item>
</md-accordion>

<md-accordion variant="outlined" default-expanded="0">
  <md-accordion-item headline="Outlined — one">A shared chassis.</md-accordion-item>
  <md-accordion-item headline="Outlined — two">Divided, not separated.</md-accordion-item>
</md-accordion>

elevation="0…5" raises the surface — in filled each item gets its own shadow, in outlined the shadow is drawn on the shared chassis. Expanded items lift one level above the resting value.

Each item takes an icon, and disabled locks one closed — it also drops out of keyboard reordering.

Leading icons, and a disabled section
Two seats, 09:40 departure. Hotel Meridian, sea view. Add a policy once travel is confirmed.
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-accordion style="--md-accordion-gap: var(--md-sys-spacing-gap-lg, 16px);">
  <md-accordion-item headline="Flights" supporting-text="Outbound and return" icon="flight">Two seats, 09:40 departure.</md-accordion-item>
  <md-accordion-item headline="Hotel" supporting-text="Three nights" icon="hotel">Hotel Meridian, sea view.</md-accordion-item>
  <md-accordion-item headline="Insurance" supporting-text="Not yet available" icon="shield" disabled>Add a policy once travel is confirmed.</md-accordion-item>
</md-accordion>

disabled locks a section closed without removing it from the page. The header is a natively disabled <button>, so it drops out of the tab order and cannot be focused — a keyboard user arrows straight past it. The label is still rendered and still readable in browse mode, so the section is visible as “present but unavailable”, but do not rely on it being reachable by Tab.

Use it for “not available yet”. It is not a privacy control: the panel content stays in the DOM (see The panel is not lazy) — leave anything the user must not see out of the markup entirely.

A disabled section between two enabled ones — Tab skips it
Click to expand. This section unlocks in v2. Click to expand.
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-accordion style="--md-accordion-gap: var(--md-sys-spacing-gap-lg, 16px);">
  <md-accordion-item headline="Available now">Click to expand.</md-accordion-item>
  <md-accordion-item headline="Coming soon" supporting-text="Available in v2" icon="lock" disabled>This section unlocks in v2.</md-accordion-item>
  <md-accordion-item headline="Also available">Click to expand.</md-accordion-item>
</md-accordion>

Three props decide what is open, and they interact.

PropEffect
default-expandedWhich items start open — a comma-separated string of indices
exclusiveOpening one item collapses any other open item
keep-one-expandedThe last open item refuses to close (its button gets aria-disabled="true")

exclusive and keep-one-expanded together give the classic “always exactly one open” behaviour:

exclusive + keep-one-expanded — try closing the open section
Theme, density, motion. Email and push. Data sharing and export.
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-accordion exclusive keep-one-expanded variant="outlined" default-expanded="0">
  <md-accordion-item headline="Appearance">Theme, density, motion.</md-accordion-item>
  <md-accordion-item headline="Notifications">Email and push.</md-accordion-item>
  <md-accordion-item headline="Privacy">Data sharing and export.</md-accordion-item>
</md-accordion>

Let the group own the state. Setting expanded on individual children to drive the group creates two sources of truth; use default-expanded for the initial state and mdToggle to observe changes.

In Storybook: controlled and uncontrolled covers driving expanded yourself versus letting the component own it, keep-one-expanded isolates the “last panel can’t close” rule, prop reactions pins what happens when exclusive or keep-one-expanded is flipped at runtime, and initial-state edge cases covers default-expanded conflicting with per-item expanded.

heading-level renders a real <h1><h6> around each item’s header button and is pushed down onto every child, overriding anything the item sets itself.

<md-accordion heading-level="2" style="--md-accordion-gap: var(--md-sys-spacing-gap-lg, 16px);"></md-accordion>

The two accordions below are pixel-identical and structurally different: the first wraps each header in a real <h2>, the second in an <h4>. Inspect a header, or run a screen reader’s heading list, and the difference is obvious — a visual review will never catch it.

heading-level 2 vs 4 — identical on screen, different in the accessibility tree
h2 Invoices and payment method. h4 Invoices and payment method.
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<div style="display:grid;grid-template-columns:auto 1fr;gap:14px 16px;align-items:center;">
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">h2</span>
  <md-accordion heading-level="2" variant="outlined">
    <md-accordion-item headline="Billing" supporting-text="Rendered inside an h2">Invoices and payment method.</md-accordion-item>
  </md-accordion>
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">h4</span>
  <md-accordion heading-level="4" variant="outlined">
    <md-accordion-item headline="Billing" supporting-text="Rendered inside an h4">Invoices and payment method.</md-accordion-item>
  </md-accordion>
</div>

The heading-level story shows the rendered outline for each value.

region controls ARIA, not rendering. Nothing appears or disappears; only the role on each panel changes.

ValuePanel role
autoDefault. region while the item count is at or below region-threshold, otherwise group
alwaysEvery panel is a region landmark
neverEvery panel is a group — still labelled by its header, never a landmark

region-threshold defaults to 6, following the APG’s advice: a landmark per panel is helpful for a handful of sections and pure noise for dozens, because every one of them shows up in the screen reader’s landmark list.

<md-accordion region="never" style="--md-accordion-gap: var(--md-sys-spacing-gap-lg, 16px);">…30 sections…</md-accordion>

Nothing below looks different, and that is the whole point: the only thing region changes is the role on each panel. The middle row has three items and a region-threshold of 2, so auto has already downgraded its panels to group.

Three region policies — visually identical, three different panel roles
auto Panels are landmark regions. Panels are landmark regions. auto (2) Panels downgrade to group. Panels downgrade to group. Panels downgrade to group. never Still labelled by its header. Still labelled by its header.
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<div style="display:grid;grid-template-columns:auto 1fr;gap:14px 16px;align-items:center;">
  <span style="inline-size:5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">auto</span>
  <md-accordion region="auto" variant="outlined">
    <md-accordion-item headline="Two sections, under the threshold">Panels are landmark regions.</md-accordion-item>
    <md-accordion-item headline="Second section">Panels are landmark regions.</md-accordion-item>
  </md-accordion>
  <span style="inline-size:5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">auto (2)</span>
  <md-accordion region="auto" region-threshold="2" variant="outlined">
    <md-accordion-item headline="Three sections, over the threshold">Panels downgrade to group.</md-accordion-item>
    <md-accordion-item headline="Second section">Panels downgrade to group.</md-accordion-item>
    <md-accordion-item headline="Third section">Panels downgrade to group.</md-accordion-item>
  </md-accordion>
  <span style="inline-size:5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">never</span>
  <md-accordion region="never" variant="outlined">
    <md-accordion-item headline="Never a landmark">Still labelled by its header.</md-accordion-item>
    <md-accordion-item headline="Second section">Still labelled by its header.</md-accordion-item>
  </md-accordion>
</div>

The region-role policy story walks every combination of region, region-threshold and item count.

transition picks the expand/collapse choreography. It is reflected to the host so item CSS can pick it up.

PresetMotion
expressiveDefault. Asymmetric easing, spring chevron, container shape and elevation morph
standardSymmetric MD3 emphasized easing, no overshoot — the neutral choice for dense UI
fadeContent cross-fades with the height change; unobtrusive for long copy and forms
collapsePlain height collapse with ease-in-out — the classic disclosure look
noneInstant snap; no transition at all
All five presets
Asymmetric easing with a shape and elevation morph. No overshoot — the neutral choice for dense UI. Unobtrusive for long copy and forms. The classic disclosure look. Instant snap.
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<div style="display: grid; gap: 20px; max-inline-size: 520px;">
  <md-accordion transition="expressive" style="--md-accordion-gap: var(--md-sys-spacing-gap-lg, 16px);"><md-accordion-item headline="expressive" supporting-text="Default — spring chevron, morphing container">Asymmetric easing with a shape and elevation morph.</md-accordion-item></md-accordion>
  <md-accordion transition="standard" style="--md-accordion-gap: var(--md-sys-spacing-gap-lg, 16px);"><md-accordion-item headline="standard" supporting-text="Symmetric emphasized easing">No overshoot — the neutral choice for dense UI.</md-accordion-item></md-accordion>
  <md-accordion transition="fade" style="--md-accordion-gap: var(--md-sys-spacing-gap-lg, 16px);"><md-accordion-item headline="fade" supporting-text="Cross-fade with the height change">Unobtrusive for long copy and forms.</md-accordion-item></md-accordion>
  <md-accordion transition="collapse" style="--md-accordion-gap: var(--md-sys-spacing-gap-lg, 16px);"><md-accordion-item headline="collapse" supporting-text="Plain height, ease-in-out">The classic disclosure look.</md-accordion-item></md-accordion>
  <md-accordion transition="none" style="--md-accordion-gap: var(--md-sys-spacing-gap-lg, 16px);"><md-accordion-item headline="none" supporting-text="No transition at all">Instant snap.</md-accordion-item></md-accordion>
</div>

Each preset ships its own pair of durations, and --md-accordion-item-expand-duration / --md-accordion-item-collapse-duration override whichever preset is active — so you keep the preset’s easing curve and change only the timing.

transitionExpandCollapse
expressive (default)350ms200ms
standard300ms300ms
fade300ms200ms
collapse300ms300ms
none

Most presets collapse at least as fast as they expand: the reader has already seen the content, so the closing move does not need to be admired.

Same easing, three speeds
Open and close me. Watch the easing stretch. Almost instant — but still eased.
Show code for each technology
<!-- index.html <head> — the icon font the components draw from -->
<link rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">

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

<style>
  .acc-slow { --md-accordion-item-expand-duration: 800ms; --md-accordion-item-collapse-duration: 600ms; }
  .acc-fast { --md-accordion-item-expand-duration: 120ms; --md-accordion-item-collapse-duration: 100ms; }
</style>
<div style="display: grid; gap: 20px; max-inline-size: 520px;">
  <md-accordion transition="standard" style="--md-accordion-gap: var(--md-sys-spacing-gap-lg, 16px);"><md-accordion-item headline="Preset timing" supporting-text="300ms open / 300ms close — no override" icon="schedule">Open and close me.</md-accordion-item></md-accordion>
  <md-accordion transition="standard" class="acc-slow" style="--md-accordion-gap: var(--md-sys-spacing-gap-lg, 16px);"><md-accordion-item headline="Slow" supporting-text="800ms open / 600ms close" icon="timer">Watch the easing stretch.</md-accordion-item></md-accordion>
  <md-accordion transition="standard" class="acc-fast" style="--md-accordion-gap: var(--md-sys-spacing-gap-lg, 16px);"><md-accordion-item headline="Fast" supporting-text="120ms open / 100ms close" icon="bolt">Almost instant — but still eased.</md-accordion-item></md-accordion>
</div>

All three above use transition="standard", so all three share its symmetric emphasized curve; only the durations differ. The first is the preset’s own 300ms/300ms, the other two override it. Set the tokens on the accordion and they cascade to its items, or scope them to a class as here.

Use transition="none" to honour prefers-reduced-motion, and in screenshot or automated-test harnesses where animation causes flake.

reorderable adds a leading drag handle to each item. Items can also be moved with Alt + ArrowUp / ArrowDown while a header is focused. Disabled items can’t be moved.

<md-accordion reorderable style="--md-accordion-gap: var(--md-sys-spacing-gap-lg, 16px);"></md-accordion>
<script type="module">
accordion.addEventListener('mdReorder', (e) => saveOrder(e.detail.order));
</script>

While an item is dragged the accordion opens a drop-target ghost in the slot it will land in — the same affordance md-list uses, so the two reorder surfaces read as one pattern. It is a drop-placeholder part, tinted from --md-accordion-drop-placeholder-*.

Drag a handle, or focus a header and press Alt+ArrowDown Open in Storybook
Two adults, 4–11 May. Hotel Meridian, sea view. Card ending 4242.
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-accordion reorderable style="--md-accordion-gap: var(--md-sys-spacing-gap-lg, 16px);">
  <md-accordion-item headline="Trip basics" supporting-text="Dates and travellers">Two adults, 4–11 May.</md-accordion-item>
  <md-accordion-item headline="Accommodation" supporting-text="Where you are staying">Hotel Meridian, sea view.</md-accordion-item>
  <md-accordion-item headline="Payment" supporting-text="How you are paying">Card ending 4242.</md-accordion-item>
</md-accordion>

Drag-reordering is pointer-driven; the Alt+arrow shortcut is the keyboard equivalent. Surface it somewhere if reordering matters to your users. The handle itself is replaceable — see custom drag handles.

floating is a specialised mode, not the default layout: the accordion becomes a position: fixed panel with a drag bar, translated by CSS transform and draggable anywhere in the viewport.

<md-accordion floating initial-x="24" initial-y="24" bring-to-front style="--md-accordion-gap: var(--md-sys-spacing-gap-lg, 16px);">
<md-accordion-item headline="Layers"></md-accordion-item>
</md-accordion>
  • initial-x / initial-y are physical viewport pixels from the top-left corner — positive initial-x always moves right, in LTR and RTL alike.
  • bring-to-front (default true) bumps z-index on drag start. Turn it off if you manage stacking yourself.
  • mdDragStart / mdDragMove / mdDragEnd fire only in this mode.
  • Width, surface and radius come from the --md-accordion-floating-* properties.

A floating accordion is position: fixed, so in a real app it is positioned against the viewport. The preview below pins it to the demo box instead by giving the wrapper a transform — an ancestor with a transform becomes the containing block for its position: fixed descendants — so initial-x / initial-y are measured from the box’s top-left corner and the panel is clipped by it. Grab the grip above the first headline and drag it around.

A floating panel, contained to the demo box — drag it by the grip
Background, artwork, annotations. Blur, contrast, grain.
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-accordion floating initial-x="24" initial-y="24" bring-to-front default-expanded="0">
  <md-accordion-item headline="Layers" supporting-text="Drag me by the grip" icon="layers">Background, artwork, annotations.</md-accordion-item>
  <md-accordion-item headline="Filters" icon="tune">Blur, contrast, grain.</md-accordion-item>
</md-accordion>

In Storybook, where it can own the whole viewport: floating, floating + reorderable, and floating under RTL.

EventCancelableDetailFires
mdToggleno{ index, expanded, expandedIndices }Any item expands or collapses
mdReorderno{ from, to, order }An item is moved by drag or Alt+arrow
mdDragStartnoMdAccordionDragDetailA floating panel starts being dragged
mdDragMovenoMdAccordionDragDetailContinuously while dragging
mdDragEndnoMdAccordionDragDetailThe floating panel is released
interface MdAccordionDragDetail {
clientX: number; // pointer, viewport coordinates
clientY: number;
x: number; // panel translation (px)
y: number;
dx: number; // distance since drag start
dy: number;
}

Listen on the accordion, not on the items — mdToggle already carries the index and the full set of expanded indices, and the items’ own mdItemRequestFocus / mdItemRequestReorder events are internal plumbing.

Expand a section, or drag a handle, and watch the log fill in. One listener on the accordion covers every item.

mdToggle and mdReorder — one listener on the accordion
Refunds land back on the original card within five working days. Switch at any time — the change applies on the next billing date. Thirty-two countries, tracked as standard.
Expand a section, or drag a handle…
Show code for each technology
<md-accordion id="faq" exclusive reorderable>
<md-accordion-item headline="How do refunds work?" icon="receipt_long">Refunds land back on the original card within five working days.</md-accordion-item>
<md-accordion-item headline="Can I change my plan?" icon="tune">Switch at any time — the change applies on the next billing date.</md-accordion-item>
<md-accordion-item headline="Where do you ship?" icon="public">Thirty-two countries, tracked as standard.</md-accordion-item>
</md-accordion>

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

faq.addEventListener('mdToggle', (e) => {
  const { index, expanded, expandedIndices } = e.detail;
  console.log('mdToggle', index, expanded, expandedIndices);
});

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

Properties

PropertyAttributeTypeDefaultReflects
variantvariant'filled' | 'outlined''filled'Yes
densitydensity0 | -1 | -2 | -3 | -40Yes
elevationelevation0 | 1 | 2 | 3 | 4 | 50Yes
reorderablereorderablebooleanfalseYes
floatingfloatingbooleanfalseYes
initialXinitial-xnumber24
initialYinitial-ynumber24
bringToFrontbring-to-frontbooleantrue
exclusiveexclusivebooleanfalseYes
keepOneExpandedkeep-one-expandedbooleanfalseYes
headingLevelheading-level1 | 2 | 3 | 4 | 5 | 63
regionregion'auto' | 'always' | 'never''auto'Yes
regionThresholdregion-thresholdnumber6
transitiontransition'expressive' | 'standard' | 'fade' | 'collapse' | 'none''expressive'Yes
defaultExpandeddefault-expandedstring''

Slots

SlotDescription
(default)

CSS Custom Properties

Override on the host element for per-instance theming:

PropertyDescription
--md-accordion-gapGap between items in filled variant
--md-accordion-outline-colorOutlined variant border colour
--md-accordion-container-shapeBorder-radius of the outer chassis
--md-accordion-elevationExplicit elevation shadow override.
--md-accordion-floating-widthWidth of the floating panel
--md-accordion-floating-bgSurface tint of the floating panel
--md-accordion-floating-elevationResting shadow for floating panel
--md-accordion-floating-radiusCorner radius of the floating chassis
--md-accordion-drop-placeholder-color
--md-accordion-drop-placeholder-outline-color
--md-accordion-drop-placeholder-outline-width
--md-accordion-drop-placeholder-shape
--md-accordion-drop-placeholder-opacity
--md-accordion-floating-elevation-active

CSS Shadow Parts

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

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

The accessibility story walks the roles, keyboard model and announced output.

  • Each item’s header is a button that toggles its panel and exposes its expanded state; the accordion wires the heading level and the panel role.
  • heading-level is a real accessibility control, not styling. A wrong level breaks document navigation. The default 3 is a guess about your page, not a fact about it.
  • region/region-threshold exist to stop landmark proliferation. A landmark per panel is noise past a handful of sections — trust auto.
  • keep-one-expanded advertises the locked-open header with aria-disabled="true" rather than silently swallowing the click.
  • Arrow keys move between headers, Home / End jump to the first and last.
  • Honour prefers-reduced-motion with transition="none".
  • Drag-reordering is pointer-only; Alt + ArrowUp / ArrowDown is the keyboard path — document it if reordering matters.

Tab into the accordion below and drive it from the keyboard: ArrowDown / ArrowUp wrap between headers, Home / End jump to the first and last, Enter or Space toggles, and Alt + ArrowUp / ArrowDown moves a section. Because keep-one-expanded is on, the one open header reports aria-disabled="true" instead of silently ignoring you.

Keyboard model — tab in, then use the arrows, Home, End and Alt+arrows
Locked open while it is the only expanded section. Arrow down to reach me, Enter to open. End jumps straight here.
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-accordion keep-one-expanded reorderable heading-level="3" default-expanded="0" variant="outlined">
  <md-accordion-item headline="Appearance" supporting-text="Theme, density, motion" icon="palette">Locked open while it is the only expanded section.</md-accordion-item>
  <md-accordion-item headline="Notifications" supporting-text="Email and push" icon="notifications">Arrow down to reach me, Enter to open.</md-accordion-item>
  <md-accordion-item headline="Privacy" supporting-text="Data sharing and export" icon="lock">End jumps straight here.</md-accordion-item>
</md-accordion>

Set dir on any ancestor and the accordion mirrors: header padding, the leading icon, the drag handle and the chevron all swap sides. Nothing on the component needs changing.

<div dir="rtl">
<md-accordion reorderable></md-accordion>
</div>

The two rows below are the same markup — only the dir on the wrapper differs. See the RTL story and RTL.

Same markup, dir=ltr vs dir=rtl
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-accordion reorderable variant="outlined">
      <md-accordion-item headline="تفاصيل الرحلة" supporting-text="التواريخ والمسافرون" icon="flight">بالغان، ٤–١١ مايو.</md-accordion-item>
      <md-accordion-item headline="الإقامة" supporting-text="مكان إقامتك" icon="hotel">فندق ميريديان، إطلالة على البحر.</md-accordion-item>
    </md-accordion>
  </div>
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">rtl</span>
  <div dir="rtl">
    <md-accordion reorderable variant="outlined">
      <md-accordion-item headline="تفاصيل الرحلة" supporting-text="التواريخ والمسافرون" icon="flight">بالغان، ٤–١١ مايو.</md-accordion-item>
      <md-accordion-item headline="الإقامة" supporting-text="مكان إقامتك" icon="hotel">فندق ميريديان، إطلالة على البحر.</md-accordion-item>
    </md-accordion>
  </div>
</div>

One thing on this component deliberately does not mirror: the floating panel’s initial-x / initial-y. They are viewport pixels from the top-left corner, so a positive initial-x moves the panel right in LTR and in RTL alike — the drag deltas come from clientX, which is also physical, so the two stay in lockstep. The panel is anchored at physical left: 0, so negating initial-x “to mirror” walks it off the left edge of the screen — which under RTL is the trailing edge, the opposite of what the mirroring was meant to achieve.

Both under dir=rtl — a negated initial-x slides off the edge
correct
initial-x="24"
wrong
initial-x="-24"
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;inline-size:100%;grid-template-columns:auto minmax(0,1fr);gap:14px 16px;align-items:center;">
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">correct</span>
  <div dir="rtl" style="position:relative;block-size:150px;overflow:hidden;transform:translateZ(0);contain:layout paint;border:1px dashed var(--md-sys-color-outline);border-radius:12px;">
    <md-accordion floating initial-x="24" initial-y="24" style="--md-accordion-floating-width: 240px;">
      <md-accordion-item headline="الطبقات" icon="layers">initial-x="24"</md-accordion-item>
    </md-accordion>
  </div>
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">wrong</span>
  <div dir="rtl" style="position:relative;block-size:150px;overflow:hidden;transform:translateZ(0);contain:layout paint;border:1px dashed var(--md-sys-color-error);border-radius:12px;">
    <md-accordion floating initial-x="-24" initial-y="24" style="--md-accordion-floating-width: 240px;">
      <md-accordion-item headline="الطبقات" icon="layers">initial-x="-24"</md-accordion-item>
    </md-accordion>
  </div>
</div>

density="-1…-4" is set once on the accordion and cascades to every item. Measured on the ladder below, where each header carries a headline and supporting text: 74 → 69 → 63 → 58 → 52px. A single-line header (headline only) starts at 64px instead, so budget by the header you actually ship rather than by the rung alone.

Density 0 through -4 — header, icon and hit area all taper
0 Panel body. -1 Panel body. -2 Panel body. -3 Panel body. -4 Panel body.
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<div style="display:grid;grid-template-columns:auto 1fr;gap:14px 16px;align-items:center;">
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">0</span>
  <md-accordion density="0" variant="outlined"><md-accordion-item headline="Rung 0" supporting-text="Same content at every rung">Panel body.</md-accordion-item></md-accordion>
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">-1</span>
  <md-accordion density="-1" variant="outlined"><md-accordion-item headline="Rung -1" supporting-text="Same content at every rung">Panel body.</md-accordion-item></md-accordion>
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">-2</span>
  <md-accordion density="-2" variant="outlined"><md-accordion-item headline="Rung -2" supporting-text="Same content at every rung">Panel body.</md-accordion-item></md-accordion>
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">-3</span>
  <md-accordion density="-3" variant="outlined"><md-accordion-item headline="Rung -3" supporting-text="Same content at every rung">Panel body.</md-accordion-item></md-accordion>
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">-4</span>
  <md-accordion density="-4" variant="outlined"><md-accordion-item headline="Rung -4" supporting-text="Same content at every rung">Panel body.</md-accordion-item></md-accordion>
</div>

The two are independent: dir decides which side everything sits on, density decides how tall it is. They compose without either one leaking into the other.

dir=rtl combined with density=-3
ltr / 0
Two adults, 4–11 May.
rtl / 0
بالغان، ٤–١١ مايو.
rtl / -3
بالغان، ٤–١١ مايو.
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:5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">ltr / 0</span>
  <div dir="ltr">
    <md-accordion density="0" reorderable variant="outlined"><md-accordion-item headline="Trip basics" supporting-text="Dates and travellers" icon="flight">Two adults, 4–11 May.</md-accordion-item></md-accordion>
  </div>
  <span style="inline-size:5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">rtl / 0</span>
  <div dir="rtl">
    <md-accordion density="0" reorderable variant="outlined"><md-accordion-item headline="تفاصيل الرحلة" supporting-text="التواريخ والمسافرون" icon="flight">بالغان، ٤–١١ مايو.</md-accordion-item></md-accordion>
  </div>
  <span style="inline-size:5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">rtl / -3</span>
  <div dir="rtl">
    <md-accordion density="-3" reorderable variant="outlined"><md-accordion-item headline="تفاصيل الرحلة" supporting-text="التواريخ والمسافرون" icon="flight">بالغان، ٤–١١ مايو.</md-accordion-item></md-accordion>
  </div>
</div>

Density — one prop on the accordion, applied to every section; the item’s density exists for standalone use. See Density.

i18n — headlines and panel content are yours to translate (the localization story switches a full set). Longer headlines wrap, so check header height per locale rather than assuming one-line headers — the responsiveness story covers narrow viewports.

Custom propertyPurposeDefault
--md-accordion-gapSpace between items in the filled variant--md-sys-spacing-gap-sm (8px)
--md-accordion-outline-coloroutlined border and internal dividers--md-sys-color-outline-variant
--md-accordion-container-shapeRadius of the outer chassismax(8px, 12px + density)
--md-accordion-elevationExplicit shadow override for the surfacethe elevation prop’s level
--md-accordion-floating-widthFloating panel width360px
--md-accordion-floating-bgFloating panel surface--md-sys-color-surface-container-low
--md-accordion-floating-radiusFloating panel corner radius--md-sys-shape-corner-large (16px)
--md-accordion-floating-elevation / -elevation-activeFloating panel shadow, at rest and while dragging--md-sys-elevation-3 / -5
--md-accordion-drop-placeholder-color / -outline-colorReorder ghost fill and dashed bordertinted primary-container / primary
--md-accordion-drop-placeholder-outline-width / -shape / -opacityReorder ghost border width, radius and opacity2px / chassis shape / 1

Item-level colours, padding and shape live on the --md-accordion-item-* properties documented on md-accordion-item — set them on the accordion and they cascade to every item.

Themed instance
Squared corners, primary outline. Same chassis.
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-accordion variant="outlined" style="--md-accordion-outline-color: var(--md-sys-color-primary); --md-accordion-container-shape: 4px;">
  <md-accordion-item headline="Themed chassis">Squared corners, primary outline.</md-accordion-item>
  <md-accordion-item headline="Second section">Same chassis.</md-accordion-item>
</md-accordion>

elevation (0–5) raises the whole chassis. It is a prop rather than a custom property because the floating mode swaps between two levels while dragging.

elevation 0, 2 and 4
Flat — the default. Lifted off the surface. Card-like.
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<div style="display: grid; gap: 24px;">
  <md-accordion elevation="0" style="--md-accordion-gap: var(--md-sys-spacing-gap-lg, 16px);"><md-accordion-item headline="elevation 0">Flat — the default.</md-accordion-item></md-accordion>
  <md-accordion elevation="2" style="--md-accordion-gap: var(--md-sys-spacing-gap-lg, 16px);"><md-accordion-item headline="elevation 2">Lifted off the surface.</md-accordion-item></md-accordion>
  <md-accordion elevation="4" style="--md-accordion-gap: var(--md-sys-spacing-gap-lg, 16px);"><md-accordion-item headline="elevation 4">Card-like.</md-accordion-item></md-accordion>
</div>

Width follows the container, so constrain the parent rather than the accordion; height is driven by content, with content-max-height on an item capping a long panel and scrolling inside it. The capped body becomes a real scroll container and takes tabindex="0", so it can be scrolled by keyboard — without that, a scrollable region with no focusable child is an axe violation.

Constrained width, and a capped panel
The accordion fills whatever box you put it in.
This panel is capped with content-max-height, so a long body scrolls inside the panel instead of pushing the rest of the page down. Keep adding copy and the panel stays the same height. The scrollbar belongs to the panel, not the page, which is what you want inside a dense settings screen where several sections may be open at once.
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<div style="display: grid; gap: 24px;">
  <div style="max-inline-size: 320px;">
    <md-accordion variant="outlined"><md-accordion-item headline="In a 320px column">The accordion fills whatever box you put it in.</md-accordion-item></md-accordion>
  </div>
  <md-accordion variant="outlined">
    <md-accordion-item headline="Capped panel" content-max-height="90px" expanded>
      This panel is capped with content-max-height, so a long body scrolls inside the panel instead of pushing the rest of the page down. Keep adding copy and the panel stays the same height. The scrollbar belongs to the panel, not the page, which is what you want inside a dense settings screen where several sections may be open at once.
    </md-accordion-item>
  </md-accordion>
</div>

For a fixed-height shell — a sidebar, a dashboard tile — cap a wrapper and let it scroll, rather than capping the accordion itself:

A capped, scrolling wrapper
Standard delivery is free over 50. Unused items in original packaging. Covers manufacturing defects. Chat and email.
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<div style="block-size: 220px; overflow: auto; padding: 8px; border: 1px dashed var(--md-sys-color-outline); border-radius: 12px; max-inline-size: 420px;">
  <md-accordion variant="outlined">
    <md-accordion-item headline="Shipping" supporting-text="Arrives in 3–5 days">Standard delivery is free over 50.</md-accordion-item>
    <md-accordion-item headline="Returns" supporting-text="30 days">Unused items in original packaging.</md-accordion-item>
    <md-accordion-item headline="Warranty" supporting-text="Two years">Covers manufacturing defects.</md-accordion-item>
    <md-accordion-item headline="Support" supporting-text="24/7">Chat and email.</md-accordion-item>
  </md-accordion>
</div>

The accordion is fluid: it fills its parent’s inline size, headlines wrap rather than truncate, and the header keeps its 48px minimum touch target however narrow the column gets.

The same item at two widths

260px

Panel body.

fluid

Panel body.
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; align-items: flex-start; flex-wrap: wrap;">
  <div style="inline-size: 260px;">
    <p style="margin: 0 0 8px; font: 500 12px system-ui, sans-serif; color: var(--md-sys-color-on-surface-variant);">260px</p>
    <md-accordion variant="outlined">
      <md-accordion-item headline="A headline long enough to wrap in a narrow column" supporting-text="Supporting text also wraps">Panel body.</md-accordion-item>
    </md-accordion>
  </div>
  <div style="flex: 1; min-inline-size: 320px;">
    <p style="margin: 0 0 8px; font: 500 12px system-ui, sans-serif; color: var(--md-sys-color-on-surface-variant);">fluid</p>
    <md-accordion variant="outlined">
      <md-accordion-item headline="A headline long enough to wrap in a narrow column" supporting-text="Supporting text also wraps">Panel body.</md-accordion-item>
    </md-accordion>
  </div>
</div>

Item-level colour and spacing are all custom properties, and the chevron can be recoloured, restyled through ::part(trailing-icon), or replaced outright by slotting your own into trailing-icon.

Custom colours, sizes and caret
The expanded header uses the tertiary role. Sizing is token-driven. A slotted chevron rotates with the same transform as the built-in one.
Show code for each technology
<!-- index.html <head> — the icon font the components draw from -->
<link rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">

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

<div style="display: grid; gap: 24px;">
  <md-accordion variant="outlined" style="--md-accordion-item-header-color-expanded: var(--md-sys-color-tertiary-container); --md-accordion-item-headline-color-expanded: var(--md-sys-color-on-tertiary-container); --md-accordion-item-icon-color-expanded: var(--md-sys-color-on-tertiary-container);">
    <md-accordion-item headline="Recoloured expanded state" icon="palette" expanded>The expanded header uses the tertiary role.</md-accordion-item>
  </md-accordion>
  <md-accordion variant="outlined" style="--md-accordion-item-header-padding: 24px; --md-accordion-item-header-min-height: 72px;">
    <md-accordion-item headline="Roomier header" supporting-text="24px padding, 72px minimum">Sizing is token-driven.</md-accordion-item>
  </md-accordion>
  <md-accordion variant="outlined">
    <md-accordion-item headline="Replaced caret">
      <svg slot="trailing-icon" viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 6l6 6-6 6"/></svg>
      A slotted chevron rotates with the same transform as the built-in one.
    </md-accordion-item>
  </md-accordion>
</div>

In Storybook: custom sizes, custom caret, custom drag handles, width and dark theme.

The accordion exposes exactly one part of its own — drop-placeholder, the ghost slot that opens where a dragged item will land. Everything else you would want to restyle belongs to the item, whose parts are listed under Item theming.

Drag a handle below and the ghost is tertiary-tinted with a solid border instead of the default dashed primary tint.

A restyled drop ghost — drag a handle to see it
Two adults, 4–11 May. Hotel Meridian, sea view. Card ending 4242.
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<style>
  .acc-ghost::part(drop-placeholder) {
  background-color: color-mix(in srgb, var(--md-sys-color-tertiary-container) 70%, transparent);
  border-style: solid;
  border-color: var(--md-sys-color-tertiary);
  }
</style>
<md-accordion class="acc-ghost" reorderable variant="outlined">
  <md-accordion-item headline="Trip basics" supporting-text="Dates and travellers">Two adults, 4–11 May.</md-accordion-item>
  <md-accordion-item headline="Accommodation" supporting-text="Where you are staying">Hotel Meridian, sea view.</md-accordion-item>
  <md-accordion-item headline="Payment" supporting-text="How you are paying">Card ending 4242.</md-accordion-item>
</md-accordion>
md-accordion::part(drop-placeholder) {
border-style: solid;
border-color: var(--md-sys-color-tertiary);
}

Each section is an md-accordion-item. It has its own props, parts and tokens, documented here rather than on a separate page: outside an accordion it is only half a component, and inside one the parent overrides several of its settings.

It does work standalone as a single collapsible region, and that is a legitimate use. What does not exist outside an accordion is group behaviour: exclusive, keep-one-expanded, arrow-key movement between headers, drag / Alt+arrow reordering, and the coordinated heading level and panel role.

A standalone item, outside any accordion

Proxy, retries and request timeout.

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

<md-accordion-item headline="Advanced options" supporting-text="Rarely needed" heading-level="2">
  <p style="margin: 0;">Proxy, retries and request timeout.</p>
</md-accordion-item>

Custom glyphs go in the leading-icon / trailing-icon slots and rich header text in the headline slot; a slot replaces the corresponding prop.

  • Inside an accordion the parent overrides heading-level and region-role. It pushes its own heading-level and its region / region-threshold decision onto every child, so setting either on the item only means something standalone.
  • mdItemRequestFocus and mdItemRequestReorder are internal plumbing. The parent listens for them to drive roving focus and Alt+arrow reordering. Listen to the accordion’s mdToggle / mdReorder instead.
  • collapsible="false" makes an open item permanent — the header advertises aria-disabled="true" and Enter / Space / click stop toggling. Combined with the parent’s keep-one-expanded you can build a section that can never close; pick one constraint, not both.
  • The panel is not lazy. Collapsed content is in the DOM, just hidden. For expensive content, render it yourself on first expand.
  • expanded is two-way bindable, but driving group state by setting it on each child fights the parent — use the accordion’s default-expanded.

Properties

PropertyAttributeTypeDefaultReflects
headlineheadlinestring''
supportingTextsupporting-textstring''
iconiconstring''
expandedexpandedbooleanfalseYes
disableddisabledbooleanfalseYes
headingLevelheading-level1 | 2 | 3 | 4 | 5 | 63
collapsiblecollapsiblebooleantrue
regionRoleregion-role| 'region' | 'group' | 'none''region'
contentMaxHeightcontent-max-heightstring''
densitydensity0 | -1 | -2 | -3 | -40Yes

Methods

MethodParameters
toggle()none
focusHeader()none

Slots

SlotDescription
(default)
leading-iconCustom leading icon.
headlineReplace the headline text with custom markup.
trailing-iconCustom trailing icon (chevron).

CSS Custom Properties

Override on the host element for per-instance theming:

PropertyDescription
--md-accordion-item-container-colorItem background
--md-accordion-item-container-shapeBorder-radius (filled variant)
--md-accordion-item-header-colorHeader bg in resting state
--md-accordion-item-header-color-expandedHeader bg when expanded.
--md-accordion-item-headline-colorHeadline text colour
--md-accordion-item-headline-color-expandedColor when expanded
--md-accordion-item-supporting-colorSupporting-text colour
--md-accordion-item-icon-colorLeading & trailing icon colour
--md-accordion-item-icon-color-expandedIcon color when expanded
--md-accordion-item-state-layer-colorHover/focus/press overlay
--md-accordion-item-header-paddingInline padding for header
--md-accordion-item-content-paddingInline padding for content area
--md-accordion-item-header-min-heightMinimum block-size of header
--md-accordion-item-expand-durationOpen animation duration
--md-accordion-item-collapse-durationClose animation duration
--md-accordion-item-elevationBox-shadow override.
--md-accordion-item-elevation-expandedShadow when the item is
--md-accordion-item-chassis-handle-colorGlyph colour of the small
--md-accordion-item-chassis-handle-bg-hoverTint shown behind the grip
--md-accordion-item-container-color-draggingOpaque fill applied to a
--md-accordion-item-container-shape-draggingBorder-radius applied to a
--md-accordion-item-container-shape-expanded
--md-accordion-item-state-layer-color-expanded
--md-accordion-item-container-color-expanded

CSS Shadow Parts

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

PartDescription
chassis-handleHit area for the chassis-drag affordance on the
chassis-handle-bar
headingThe h3 that wraps the header button.
headerThe clickable header button.
state-layerHover/focus/press overlay.
drag-handleDrag grip shown only when the parent
leading-iconLeading icon container.
headlineHeadline text.
supporting-textSupporting caption.
trailing-iconTrailing chevron container.
panelThe region containing the content.
contentInner content wrapper (padding lives here).
content-inner

Set these on the parent accordion so every section stays uniform — they cascade.

Custom propertyPurpose
--md-accordion-item-container-color / -container-color-expandedItem surface by state
--md-accordion-item-container-shape / -container-shape-expandedCorner radius by state
--md-accordion-item-header-color / -header-color-expandedHeader background by state
--md-accordion-item-headline-color / -headline-color-expandedHeadline text
--md-accordion-item-supporting-colorSupporting text
--md-accordion-item-icon-color / -icon-color-expandedIcons
--md-accordion-item-state-layer-color / -state-layer-color-expandedHover and press overlay
--md-accordion-item-header-padding / -header-min-heightHeader metrics
--md-accordion-item-content-paddingPanel padding
--md-accordion-item-elevation / -elevation-expandedItem shadow by state
--md-accordion-item-expand-duration / -collapse-durationMotion timing

Item CSS partsheading, header, headline, supporting-text, leading-icon, trailing-icon, state-layer, panel, content, content-inner, drag-handle, chassis-handle, chassis-handle-bar.

md-accordion-item::part(headline) {
letter-spacing: 0.1px;
}

md-tabs · md-tab-panel · md-list · md-card · md-divider

For AI Agents — md-accordion

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

# md-accordion

<!-- llm:meta
tag: md-accordion
category: containment
status: custom
m3-guidelines: none — M3 has no accordion page
m3-derived-from: https://m3.material.io/components/lists/guidelines
form-associated: false
depends-on: none
used-by: none
accepts-children: md-accordion-item
-->

**Progressive disclosure for a set of sections.** Groups `md-accordion-item`
children, coordinates which are expanded, wires their heading level and panel
roles, and adds optional drag/keyboard reordering plus a draggable floating
panel mode.

> ⚠️ **Not a Material Design 3 component.** M3 has no accordion page; the
> Do/Don't table below is house rules derived from M3's list and card guidance
> plus this component's actual behaviour.

> Setup, theming, density and i18n are configured once for the whole library —
> see the library-wide specification, shipped next to these manuals as
> `main-llm.md` at the root of the `@awc-ui/core` package.

---

## When to use

- Several **independent sections** where the user only needs one or two at a
  time: FAQs, settings groups, filter categories.
- Long content that would overwhelm the page fully expanded.
- Sections the user may want to reorder (`reorderable`).

## When NOT to use

| Situation | Use instead |
|---|---|
| Switching between sibling views | `md-tabs` |
| A linear, ordered process | `md-stepper` |
| A list of records | `md-list` |
| Content users need to compare side by side | Show it all, or `md-tabs` |
| A single collapsible region | One standalone `md-accordion-item` |
| Navigation | `md-navigation-rail` / `md-navigation-bar` |
| Hiding critical information | Nothing — keep it visible |

## Decision cues

| Need | Setting |
|---|---|
| Only one section open at a time | `exclusive` |
| Never allow all sections closed | `keep-one-expanded` |
| Exactly one section always open | `exclusive keep-one-expanded` |
| Open some sections initially | `default-expanded="0,2"` (comma-separated indices) |
| Tonal tiles | `variant="filled"` (default) |
| One shared outlined chassis | `variant="outlined"` |
| Raised surface | `elevation="1"` … `elevation="5"` |
| Drag / Alt+Arrow reordering | `reorderable` |
| Draggable floating panel | `floating` (+ `initial-x` / `initial-y` / `bring-to-front`) |
| Correct heading depth for the page | `heading-level="2"` … `heading-level="6"` |
| Fewer ARIA landmarks on a long accordion | `region="never"`, or **lower** `region-threshold` (higher keeps more panels as landmarks) |
| Calmer or no motion | `transition="standard\|fade\|collapse\|none"` (default `expressive`) |

## API contract

```html
<md-accordion
  variant="filled|outlined"                          <!-- default: filled -->
  exclusive                                          <!-- default: false -->
  keep-one-expanded                                  <!-- default: false -->
  default-expanded="0,2"                             <!-- default: "" -->
  heading-level="1|2|3|4|5|6"                        <!-- default: 3 -->
  region="auto|always|never"                         <!-- default: auto -->
  region-threshold="6"                               <!-- default: 6 -->
  transition="expressive|standard|fade|collapse|none" <!-- default: expressive -->
  reorderable                                        <!-- default: false -->
  floating                                           <!-- default: false -->
  initial-x="24"                                     <!-- default: 24 -->
  initial-y="24"                                     <!-- default: 24 -->
  bring-to-front                                     <!-- default: true -->
  elevation="0|1|2|3|4|5"                            <!-- default: 0 -->
  density="-1|-2|-3|-4"                              <!-- default: 0 (uncompacted; only -1…-4 have rules) -->
>
  <md-accordion-item headline="Shipping">Arrives in 3-5 days.</md-accordion-item>
  <md-accordion-item headline="Payment">Cards and bank transfer.</md-accordion-item>
</md-accordion>
```

**Events**

| Event | Detail |
|---|---|
| `mdToggle` | `{ index: number; expanded: boolean; expandedIndices: number[] }` |
| `mdReorder` | `{ from: number; to: number; order: number[] }` |
| `mdDragStart` / `mdDragMove` / `mdDragEnd` | `MdAccordionDragDetail` = `{ clientX, clientY, x, y, dx, dy }` |

All are default Stencil events (bubbling and composed). The three drag events
fire only in `floating` mode, for the panel-chassis drag — item reordering
reports through `mdReorder`, not through them.

**Methods** — none. Toggle state through the items (`md-accordion-item.toggle()`
or its `expanded` prop).

**Slots** — one unnamed default slot, for `md-accordion-item` children. Only
direct `md-accordion-item` children are collected; anything else is rendered
but not coordinated.

**Parts** — `drop-placeholder` (the dashed ghost slot shown while an item is
being dragged).

### Behavioral contract worth knowing

- `default-expanded` is a **comma-separated string of indices** (`"0,2"`), not
  an array and not ids. It is applied once, at mount; changing it later does
  nothing. An item's own `expanded` attribute counts too, and the two are
  merged.
- The initial state is resolved before anything is applied: under `exclusive`
  the **lowest** index wins and the rest collapse; under `keep-one-expanded`
  with nothing open, the first non-disabled item is forced open.
- `exclusive` collapses the others when one opens. Switching `exclusive` on at
  runtime keeps the first expanded item and collapses the rest.
- `keep-one-expanded` locks the last open panel: its header gets
  `aria-disabled="true"` and click / Enter / Space stop toggling it. The lock
  is recomputed after every toggle.
- **`region` controls ARIA only, never rendering.** `auto` gives each panel
  `role="region"` while the item count is at or below `region-threshold`
  (default 6) and downgrades to `role="group"` above it; `always` forces
  `region`; `never` forces `group`. Panels stay labelled by their header in
  every case.
- **The accordion pushes `heading-level`, the panel role and the
  collapsible lock onto its items**, overwriting whatever the item declares.
  Set them on the accordion, not on the item.
- **Keyboard is fully supported.** With a header focused: Enter / Space toggle,
  ArrowDown / ArrowUp move focus to the next / previous header (wrapping),
  Home / End jump to the first / last, and — when `reorderable` is set —
  `Alt + ArrowUp` / `Alt + ArrowDown` move the item.
- `reorderable` **does reorder the DOM itself** (it moves the item element and
  re-indexes the group) and then emits `mdReorder`. What it does not do is
  persist that order — save `event.detail.order` yourself.
- `mdReorder.detail.order` is expressed against the order captured at mount:
  moving the item that started at index 0 to index 2 yields `[1, 2, 0]`.
- A pointer reorder starts on an item's drag handle, needs at least two items,
  and skips items carrying `disabled`.
- `floating` makes the host `position: fixed` and translates it by
  `initial-x` / `initial-y`, which are **physical** viewport pixels from the
  top-left corner in both LTR and RTL. The drag grip is rendered on whichever
  item is currently first, and `bring-to-front` bumps the host `z-index` on
  drag start.
- The host is `role="presentation"` — all the semantics live on the items.
- `transition="none"` disables every expand/collapse animation unconditionally.
  You do **not** need it for reduced motion: the stylesheets already answer
  `@media (prefers-reduced-motion: reduce)` by neutralising the item's expand /
  collapse, header, icon and state-layer transitions, plus the reorder drop
  placeholder and the floating panel's motion. Reach for `transition="none"`
  only when you want the instant snap for everyone.

---

## Do / Don't

House rules, informed by
[M3 · Lists · Guidelines](https://m3.material.io/components/lists/guidelines) —
no M3 accordion page exists.

| ✅ Do | ❌ Don't |
|---|---|
| Use it for independent, skippable sections | Don't hide information users need at a glance |
| Set `heading-level` to fit the page outline | Don't leave every accordion at `3` regardless of context |
| Use `exclusive` when sections are alternatives | Don't force `exclusive` when users need to compare two sections |
| Keep headlines short and scannable | Don't write sentence-long headlines |
| Let `region="auto"` manage landmarks | Don't force `always` on a 30-section accordion |
| Persist the order after `mdReorder` | Don't assume the component saved it |
| Let the built-in `prefers-reduced-motion` handling do its job | Don't hand-roll a `matchMedia` listener to set `transition="none"` |
| Keep sections structurally parallel | Don't mix a form, a table and prose across sibling sections |

---

## Patterns

```html
<!-- FAQ: one open at a time, first one open on load -->
<md-accordion id="faq" exclusive heading-level="2" default-expanded="0">
  <md-accordion-item headline="How do refunds work?">
    <p>Within 30 days of delivery.</p>
  </md-accordion-item>
  <md-accordion-item headline="Can I change my plan?">
    <p>Yes, at any time.</p>
  </md-accordion-item>
</md-accordion>

<script type="module">
  document.getElementById('faq').addEventListener('mdToggle', (e) => {
    // { index, expanded, expandedIndices }
    console.log(e.detail.index, e.detail.expanded, e.detail.expandedIndices);
  });
</script>
```

```html
<!-- Settings groups: exactly one section is always open -->
<md-accordion exclusive keep-one-expanded variant="outlined" heading-level="2">
  <md-accordion-item headline="Account" icon="person">…</md-accordion-item>
  <md-accordion-item headline="Notifications" icon="notifications">…</md-accordion-item>
  <md-accordion-item headline="Privacy" icon="lock">…</md-accordion-item>
</md-accordion>
```

```html
<!-- Reorderable sections: the DOM moves, you persist the order -->
<md-accordion id="sections" reorderable>
  <md-accordion-item headline="Summary">…</md-accordion-item>
  <md-accordion-item headline="Details">…</md-accordion-item>
  <md-accordion-item headline="History">…</md-accordion-item>
</md-accordion>

<script type="module">
  document.getElementById('sections').addEventListener('mdReorder', (e) => {
    // e.detail.order is the new sequence of the ORIGINAL indices, e.g. [1, 2, 0]
    localStorage.setItem('section-order', JSON.stringify(e.detail.order));
  });
</script>
```

```html
<!-- Many sections: avoid a landmark per panel -->
<md-accordion region="never" heading-level="2">
  <md-accordion-item headline="Section 1">…</md-accordion-item>
  <!-- …30 more… -->
</md-accordion>
```

```html
<!-- Calmer motion for everyone. Reduced motion needs no code: the component's
     own CSS already neutralises the animation under
     @media (prefers-reduced-motion: reduce). -->
<md-accordion transition="collapse">
  <md-accordion-item headline="Details">…</md-accordion-item>
</md-accordion>
```

```html
<!-- Floating, draggable panel -->
<md-accordion id="inspector" floating initial-x="48" initial-y="96"
              variant="outlined">
  <md-accordion-item headline="Layers">…</md-accordion-item>
  <md-accordion-item headline="Styles">…</md-accordion-item>
</md-accordion>

<script type="module">
  document.getElementById('inspector')
    .addEventListener('mdDragEnd', (e) => {
      localStorage.setItem('inspector-pos', JSON.stringify([e.detail.x, e.detail.y]));
    });
</script>
```

## Anti-patterns

| ❌ Wrong | ✅ Right | Why |
|---|---|---|
| `default-expanded="['0','2']"` | `default-expanded="0,2"` | It is a comma-separated string of indices. |
| Changing `default-expanded` to drive state after mount | Set `expanded` on the item, or call its `toggle()` | It is applied once, at mount. |
| Reading `e.detail.expanded` as the whole group state | Use `e.detail.expandedIndices` | `expanded` describes only the item that just toggled. |
| Listening for `mdDragStart` / `mdDragMove` / `mdDragEnd` to track reordering | Listen for `mdReorder` | The drag events belong to the `floating` chassis, not to item reordering. |
| Expecting `mdReorder` to persist the order | Save `e.detail.order` | The component reorders the DOM but stores nothing. |
| Setting `heading-level` / `region-role` on the items | Set `heading-level` / `region` on the accordion | The parent pushes its values onto every item. |
| `region="always"` on 30 sections | `auto` or `never` | A landmark per section is noise for assistive tech. |
| Adding your own keyboard reorder shortcut | Use the built-in `Alt + ArrowUp` / `Alt + ArrowDown` | It is already implemented when `reorderable` is set. |
| `keep-one-expanded` with every item `disabled` | Leave at least one enabled | There is then nothing the group can force open. |
| Accordion for sibling views | `md-tabs` | Different navigation model. |
| Accordion for an ordered process | `md-stepper` | Steps have sequence and validation. |
| Non-`md-accordion-item` children | Only `md-accordion-item` at the top level | Other elements render but are never coordinated. |
| Hiding critical content behind a collapsed section | Keep it visible | Collapsed content is easy to miss. |
| `exclusive` for sections users compare | Allow multiple | Forces constant re-opening. |

## Accessibility, RTL, density, i18n

**Accessibility**
- The host is `role="presentation"`; each item renders a real `<h1>`–`<h6>`
  wrapping a `<button>` with `aria-expanded` and `aria-controls`, and a panel
  labelled by that button.
- `heading-level` is a real accessibility control, not styling — a wrong level
  breaks heading navigation. Set it to match the surrounding outline.
- The `region` threshold exists because a landmark per panel becomes noise past
  a handful of sections; trust `auto` unless you have a reason.
- Full keyboard support: Enter / Space toggle, ArrowUp / ArrowDown move between
  headers with wrapping, Home / End jump to the ends, and `Alt + ArrowUp` /
  `Alt + ArrowDown` reorder when `reorderable` is set. Reordering is therefore
  **not** pointer-only.
- A panel locked open by `keep-one-expanded` stays focusable and announces
  `aria-disabled="true"`, so screen-reader users can tell it is the open one.
  A `disabled` item's header is natively disabled and skipped entirely.
- `prefers-reduced-motion: reduce` is honoured by the component's own CSS —
  expand / collapse, header, icon, drop-placeholder and floating-panel motion
  are all neutralised. There is nothing to wire up.

**RTL** — item layout, drag handles and padding are logical and mirror under
`dir="rtl"`. `floating`'s `initial-x` / `initial-y` are the exception: they are
physical viewport coordinates from the top-left corner in both directions.

**Density** — set `density="-1"` … `density="-4"` for a local rung, or inherit a
global `data-density` ancestor. Rung `0` is the uncompacted default and has no
rule of its own, so `density="0"` does **not** opt an accordion out of an
inherited rung; use `style="--md-sys-density-scale: 0"` to reset the scale
locally. The rung set on the accordion cascades to its items.

**i18n** — headlines, supporting text and panel content are yours to translate;
longer headlines wrap, so check header height per locale.

## Related components

`md-accordion-item` · `md-tabs` · `md-stepper` · `md-list` · `md-card` ·
`md-divider`

## Theming

| Custom property | Purpose | Default |
|---|---|---|
| `--md-accordion-gap` | Gap between items (`filled` variant) | `--md-sys-spacing-gap-sm` (8px) |
| `--md-accordion-outline-color` | `outlined` chassis border | `--md-sys-color-outline-variant` |
| `--md-accordion-container-shape` | Corner radius of the outer chassis | `max(8px, 12px + density × 1px)` |
| `--md-accordion-elevation` | Explicit shadow; wins over the `elevation` attribute | `--md-sys-elevation-0` |
| `--md-accordion-floating-width` | Width of the floating panel | `360px` |
| `--md-accordion-floating-bg` | Surface tint of the floating panel | `--md-sys-color-surface-container-low` |
| `--md-accordion-floating-radius` | Corner radius of the floating chassis | `--md-sys-shape-corner-large` (16px) |
| `--md-accordion-floating-elevation` | Resting shadow of the floating panel | `--md-sys-elevation-3` |
| `--md-accordion-floating-elevation-active` | Shadow while the floating panel is dragged | `--md-sys-elevation-5` |
| `--md-accordion-drop-placeholder-color` | Ghost-slot fill during a reorder drag | 45% `--md-sys-color-primary-container` |
| `--md-accordion-drop-placeholder-outline-color` | Ghost-slot dashed border colour | 55% `--md-sys-color-primary` |
| `--md-accordion-drop-placeholder-outline-width` | Ghost-slot border width | `2px` |
| `--md-accordion-drop-placeholder-shape` | Ghost-slot corner radius | `--md-accordion-container-shape` |
| `--md-accordion-drop-placeholder-opacity` | Ghost-slot opacity | `1` |

Per-item surface, header, icon and motion properties are read by
`md-accordion-item`'s own stylesheet — see its Theming table. Because custom
properties inherit, you can still set them on `<md-accordion>` to apply them to
every item.

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

```css
md-accordion.settings {
  --md-accordion-gap: 4px;
  --md-accordion-outline-color: var(--md-sys-color-outline);
}

md-accordion.settings::part(drop-placeholder) {
  --md-accordion-drop-placeholder-outline-width: 1px;
}
```

<!-- Auto Generated Below -->


## Properties

| Property          | Attribute           | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | Type                                                           | Default        |
| ----------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | -------------- |
| `bringToFront`    | `bring-to-front`    | When a floating accordion starts being dragged, bring it to the front by bumping `z-index`. Disable if you manage stacking yourself.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    | `boolean`                                                      | `true`         |
| `defaultExpanded` | `default-expanded`  | Comma-separated list of item indexes that should be expanded on first render. Items with `expanded` set in their own attribute also count.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | `string`                                                       | `''`           |
| `density`         | `density`           | Density scale: `0` (default, 56 dp headers), `-1` (48 dp), `-2` (40 dp).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | `-1 \| -2 \| -3 \| -4 \| 0`                                    | `0`            |
| `elevation`       | `elevation`         | MD3 elevation level applied to the surface — `0` (default, flat) through `5` (highest). In the `filled` variant each item gets its own shadow; in the `outlined` variant the shadow is drawn on the shared chassis. Expanded items lift to the next elevation level for an expressive "lift off the page" affordance.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | `0 \| 1 \| 2 \| 3 \| 4 \| 5`                                   | `0`            |
| `exclusive`       | `exclusive`         | Whether only one item may be expanded at a time. When `true`, expanding an item collapses any other expanded item.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `boolean`                                                      | `false`        |
| `floating`        | `floating`          | Render the accordion as a free-floating panel that can be dragged anywhere on the screen via the top drag-bar. The panel uses `position: fixed` and translates via CSS transform, so it lifts out of normal document flow.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | `boolean`                                                      | `false`        |
| `headingLevel`    | `heading-level`     | ARIA heading level for each item's heading wrapper (`<h1>`–`<h6>`). Pick the value matching the page's information architecture — e.g. `2` inside a top-level page section, `4` deep inside a card. APG: "aria-level that is appropriate for the information architecture of the page". Default `3`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    | `1 \| 2 \| 3 \| 4 \| 5 \| 6`                                   | `3`            |
| `initialX`        | `initial-x`         | Initial X translation (px) for floating panels — measured in PHYSICAL viewport coordinates from the top-left corner of the viewport, so a positive value always moves the panel to the right regardless of writing direction (LTR or RTL). Default: 24.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | `number`                                                       | `24`           |
| `initialY`        | `initial-y`         | Initial Y translation (px) for floating panels — measured in physical viewport coordinates from the top-left corner. Default: 24.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       | `number`                                                       | `24`           |
| `keepOneExpanded` | `keep-one-expanded` | Require at least one item to remain expanded. APG variant: the implementation does not permit the currently-open panel to be collapsed when doing so would leave zero panels open. The locked- open button advertises `aria-disabled="true"`. Pairs naturally with `exclusive` to enforce "exactly one panel is always open".                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           | `boolean`                                                      | `false`        |
| `region`          | `region`            | Region-role policy for panels: - `auto` (default) — applies `role="region"` only when the item   count is at or below `region-threshold`, otherwise downgrades   to `role="group"` to avoid landmark proliferation (per APG   recommendation for accordions with more than ~6 panels). - `always` — every panel is a landmark region. - `never` — no panel carries a region role (still labelled via   `aria-labelledby` though).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       | `"always" \| "auto" \| "never"`                                | `'auto'`       |
| `regionThreshold` | `region-threshold`  | Threshold above which `region="auto"` downgrades panel roles from `region` to `group`. APG suggests ~6.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | `number`                                                       | `6`            |
| `reorderable`     | `reorderable`       | Allow drag-to-reorder of items via the leading drag handles. Items can also be reordered with `Alt + ArrowUp / ArrowDown` while the header is focused. Disabled items can't be moved.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | `boolean`                                                      | `false`        |
| `transition`      | `transition`        | Motion preset applied when items expand and collapse. The prop is reflected so the item CSS can pick it up via `:host-context(md-accordion[transition="…"])`.  - `expressive` (default) — full MD3 Expressive choreography:    asymmetric easing (decelerate-on-open, accelerate-on-close),    spring-spatial chevron rotation, container shape & elevation    morph, leading-icon FILL-axis swap. Use for hero surfaces,    settings shells, anything that wants to feel premium. - `standard` — MD3 standard easing (symmetric `emphasized`) with    no spring overshoot. Container morph still happens. The    "neutral" preset — good default for dense product UI. - `fade` — content fades in/out alongside the height animation    using shorter `standard` easing. Chevron rotates without a    spring. Best for content-heavy panels (forms, long copy)    where the motion should be unobtrusive. - `collapse` — pure height collapse with simple `ease-in-out`    timing — no spring, no morph, no fade. The classic    "browser disclosure" look. - `none` — disables every transition (instant snap). Useful for    automated tests, screenshot harnesses, or environments where    motion is undesirable. | `"collapse" \| "expressive" \| "fade" \| "none" \| "standard"` | `'expressive'` |
| `variant`         | `variant`           | Visual variant. Both share the same expand/collapse mechanics. - `filled` (default) — each item is a tonal surface tile. - `outlined` — items share a single rounded chassis with an outline.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           | `"filled" \| "outlined"`                                       | `'filled'`     |


## Events

| Event         | Description                                                                                                                                                                                                                                    | Type                                                                            |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| `mdDragEnd`   | Emitted when a floating panel is released.                                                                                                                                                                                                     | `CustomEvent<MdAccordionDragDetail>`                                            |
| `mdDragMove`  | Emitted continuously while a floating panel is being dragged.                                                                                                                                                                                  | `CustomEvent<MdAccordionDragDetail>`                                            |
| `mdDragStart` | Emitted once a floating panel starts being dragged.                                                                                                                                                                                            | `CustomEvent<MdAccordionDragDetail>`                                            |
| `mdReorder`   | Emitted when items are reordered via drag-and-drop or keyboard (Alt+ArrowUp / Alt+ArrowDown). `order` is the new index sequence relative to the original order, e.g. moving the item that was at index 0 to index 2 yields `order: [1, 2, 0]`. | `CustomEvent<{ from: number; to: number; order: number[]; }>`                   |
| `mdToggle`    | Emitted when an item expands or collapses. Detail carries the full set of expanded indices.                                                                                                                                                    | `CustomEvent<{ index: number; expanded: boolean; expandedIndices: number[]; }>` |


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

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