Skip to content

Side Sheet

Supplementary content along the side of the screen. Standard (inline, sits beside your content) or modal (over a scrim), on either logical edge, with optional dividers, an actions row, a back affordance for nested flows, and its own density rung.

Live preview Open in Storybook
Show filters Reset Apply
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-button id="ss-hero-btn" variant="filled" icon="filter_list">Show filters</md-button>

<md-side-sheet id="ss-hero" variant="modal" side="end" headline="Filters" top-divider bottom-divider>
  <label><md-checkbox checked></md-checkbox> In stock</label>
  <label><md-checkbox></md-checkbox> On sale</label>
  <md-button slot="actions" variant="text">Reset</md-button>
  <md-button slot="actions" variant="filled">Apply</md-button>
</md-side-sheet>

<script type="module">
  var sheet = document.getElementById('ss-hero');
  var btn = document.getElementById('ss-hero-btn');

  btn.addEventListener('mdClick', function () { sheet.show(); });
  sheet.addEventListener('mdClose', function () { btn.focus({ preventScroll: true }); });

  document.querySelectorAll('[data-ss-hero-close]').forEach(function (b) {
    b.addEventListener('mdClick', function () { sheet.close(); });
  });
</script>

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


<md-side-sheet></md-side-sheet>
  • Desktop / large-screen supplementary content: filters, details, help, a properties panel.
  • Content the user consults alongside the main view (variant="standard").
  • A focused side task over the content (variant="modal").
  • Nested navigation within the panel (show-back).
SituationUse instead
Mobile supplementary contentmd-bottom-sheet
A blocking decisionmd-dialog
Brief feedbackmd-snackbar
App-level destination navigationmd-navigation-rail / md-navigation-bar
A short action listmd-menu
Primary content the task depends onA page, or a md-card in the main column
NeedSetting
Sits beside content, no scrimvariant="standard" (default)
Over the content with a scrimvariant="modal"
Trailing edge (M3’s usual choice)side="end" (default)
Leading edgeside="start"
Floating, inset from the edgedetached
Back arrow for a nested viewshow-back (+ mdBack)
Prevent click-away (modal)scrim-dismissible="false"

A standard sheet is inline, so it can safely be rendered open in a page like this one — the demos below do exactly that. A modal sheet covers the viewport and locks page scroll, so every modal demo on this page opens from a button instead.

The standard sheet — inline, no scrim Open in Storybook

The default: a standard sheet on the inline-end edge, sharing the layout box with your content.

Apply
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: 16px; align-items: stretch; min-block-size: 180px;">
  <div style="flex: 1; padding: 8px; color: var(--md-sys-color-on-surface-variant);">
    <p style="margin: 0;">The default: a standard sheet on the inline-end edge, sharing the layout box with your content.</p>
  </div>
  <md-side-sheet variant="standard" side="end" headline="Filters" open style="flex: 0 0 auto; --md-side-sheet-width: 240px; --md-side-sheet-max-width: 240px;">
    <label style="display:flex; align-items:center; gap:8px; padding-block:4px;"><md-checkbox checked></md-checkbox> In stock</label>
    <label style="display:flex; align-items:center; gap:8px; padding-block:4px;"><md-checkbox></md-checkbox> On sale</label>
    <md-button slot="actions" variant="filled">Apply</md-button>
  </md-side-sheet>
</div>
A standard sheet on the start side Open in Storybook

Anchored to the inline-start edge.

Main content sits beside it — a standard sheet never covers the page.

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: 16px; align-items: stretch; min-block-size: 200px;">
  <md-side-sheet variant="standard" side="start" headline="Navigation" open style="flex: 0 0 auto; --md-side-sheet-width: 220px; --md-side-sheet-max-width: 220px;">
    <p style="margin: 0; font-size: 14px;">Anchored to the inline-start edge.</p>
  </md-side-sheet>
  <div style="flex: 1; padding: 8px; color: var(--md-sys-color-on-surface-variant);">
    <p style="margin: 0;">Main content sits beside it — a standard sheet never covers the page.</p>
  </div>
</div>
A wide sheet — widen it with --md-side-sheet-width Open in Storybook

A wide sheet suits editing forms and detail views that would feel cramped at the default width.

Cancel Save
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: 16px; align-items: stretch; min-block-size: 200px;">
  <div style="flex: 1; padding: 8px; color: var(--md-sys-color-on-surface-variant);">
    <p style="margin: 0;">A wide sheet suits editing forms and detail views that would feel cramped at the default width.</p>
  </div>
  <md-side-sheet variant="standard" headline="Edit product" open style="flex: 0 0 auto; --md-side-sheet-width: 380px; --md-side-sheet-max-width: 380px;">
    <div style="display: grid; gap: 16px;">
      <md-text-field label="Name" variant="outlined" style="inline-size: 100%;"></md-text-field>
      <md-text-field label="SKU" variant="outlined" style="inline-size: 100%;"></md-text-field>
    </div>
    <md-button slot="actions" variant="text">Cancel</md-button>
    <md-button slot="actions" variant="filled">Save</md-button>
  </md-side-sheet>
</div>
Modal variants — end, start, and a detached sheet with a back arrow Open in Storybook
Open modal sheet (end) Open modal sheet (start) Open detached, with back arrow

Order #48211 · 3 items

A modal sheet takes over the side task: it traps focus, dims the page behind a scrim, and closes on Escape or a scrim click.

Cancel Track order

side is logical, so this panel lands on the leading edge and follows text direction automatically.

Close

show-back only emits mdBack — it does not navigate. You swap the panel content yourself. This one is also detached, so it floats inset from the edge with all four corners rounded.

Close
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-button id="ss-modal-open" variant="filled" icon="info">Open modal sheet</md-button>

<md-side-sheet id="ss-modal" variant="modal" side="end" headline="Order details" top-divider bottom-divider>
  <p>Order #48211 · 3 items</p>
  <md-button slot="actions" variant="text">Cancel</md-button>
  <md-button slot="actions" variant="filled">Track order</md-button>
</md-side-sheet>

<md-side-sheet id="ss-modal-back" variant="modal" side="end" headline="Advanced filters" show-back detached>
  <p>Detached floats inset from the edge, with all four corners rounded.</p>
</md-side-sheet>

<script type="module">
  var pairs = [
    ['ss-modal-open', 'ss-modal'],
    ['ss-modal-start-open', 'ss-modal-start'],
    ['ss-modal-back-open', 'ss-modal-back'],
  ];

  pairs.forEach(function (pair) {
    var btn = document.querySelector('#' + pair[0]);
    var sheet = document.querySelector('#' + pair[1]);
    btn.addEventListener('mdClick', function () { sheet.show(); });
    sheet.addEventListener('mdClose', function () { btn.focus({ preventScroll: true }); });
  });

  document.querySelectorAll('[data-ss-close]').forEach(function (b) {
    b.addEventListener('mdClick', function () {
      document.querySelector('#' + b.getAttribute('data-ss-close')).close();
    });
  });

  document.getElementById('ss-modal-back').addEventListener('mdBack', function (e) {
    e.target.headline = 'Filters';
    e.target.showBack = false;
  });
</script>
SlotContent
(default)Panel body — scrolls vertically when it overflows
headlineCustom headline content (replaces the headline prop)
backCustom back glyph (modal only, with show-back)
closeCustom close affordance — only rendered while closeable is true; with closeable="false" the whole close wrapper, slot included, is dropped
actionsBottom action bar

top-divider and bottom-divider add rules between the header, the body and the actions row — useful once the body scrolls. The actions row and its bottom divider only render when something is actually slotted into actions.

Slotted headline, dividers, scrolling body, actions row Open in Storybook
Toggle panel

This standard sheet uses a slotted headline, dividers on both edges, a long scrolling body and a two-button actions row.

Settings Cancel Save
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-side-sheet variant="standard" side="start" top-divider bottom-divider aria-label="Settings panel" open>
  <span slot="headline">
    <span class="material-symbols-outlined" aria-hidden="true">settings</span>
    Settings
  </span>

  <label>Notifications <md-switch selected></md-switch></label>
  <label>Auto-sync <md-switch></md-switch></label>

  <md-button slot="actions" variant="text">Cancel</md-button>
  <md-button slot="actions" variant="filled">Save</md-button>
</md-side-sheet>

<script type="module">
  var sheet = document.getElementById('ss-slots');
  var toggle = document.getElementById('ss-slots-toggle');

  toggle.addEventListener('mdClick', function () {
    if (sheet.open) sheet.close();
    else sheet.show();
  });
  sheet.addEventListener('mdClose', function () { toggle.focus({ preventScroll: true }); });
</script>
With and without dividers Open in Storybook

Rules under the header and above the actions.

Reset

The same panel with both dividers off — the default.

Reset
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: 16px; align-items: stretch; block-size: 260px;">
  <md-side-sheet variant="standard" headline="Dividers" top-divider bottom-divider open style="flex: 0 0 auto; --md-side-sheet-width: 240px; --md-side-sheet-max-width: 240px;">
    <p style="margin: 0; font-size: 14px;">Rules under the header and above the actions.</p>
    <md-button slot="actions" variant="text">Reset</md-button>
  </md-side-sheet>
  <md-side-sheet variant="standard" headline="No dividers" open style="flex: 0 0 auto; --md-side-sheet-width: 240px; --md-side-sheet-max-width: 240px;">
    <p style="margin: 0; font-size: 14px;">The same panel with both dividers off — the default.</p>
    <md-button slot="actions" variant="text">Reset</md-button>
  </md-side-sheet>
</div>
A slotted headline Open in Storybook
Slotted headline

Markup in the headline slot replaces the plain headline prop.

Use the slot when the headline needs an icon, a count or a truncating title; use the prop for plain text. A slotted headline does not name the panel — aria-labelledby is wired from the headline prop only — so pair it with aria-label.

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

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

<div style="display: flex; gap: 16px; align-items: stretch; min-block-size: 180px;">
  <md-side-sheet variant="standard" open aria-label="Slotted headline" style="flex: 0 0 auto; --md-side-sheet-width: 260px; --md-side-sheet-max-width: 260px;">
    <span slot="headline" style="display: inline-flex; align-items: center; gap: 8px;">
      <span class="material-symbols-outlined" aria-hidden="true" style="font-size: 20px;">tune</span>
      Slotted headline
    </span>
    <p style="margin: 0; font-size: 14px;">Markup in the headline slot replaces the plain <code>headline</code> prop.</p>
  </md-side-sheet>
  <div style="flex: 1; padding: 8px; color: var(--md-sys-color-on-surface-variant);">
    <p style="margin: 0;">Use the slot when the headline needs an icon, a count or a truncating title; use the prop for plain text. A slotted headline does not name the panel — aria-labelledby is wired from the headline prop only — so pair it with aria-label.</p>
  </div>
</div>
A scrolling body between a pinned header and actions Open in Storybook

Only the body scrolls — the header and the actions row stay pinned.

Keep adding copy and this region grows a scrollbar of its own.

Another paragraph, so the scroll is unmistakable.

And one more for good measure.

The dividers mark where the scroll region begins and ends.

Apply

Scroll inside the panel — the header, the dividers and the actions row do not move.

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: 16px; align-items: stretch; block-size: 240px;">
  <md-side-sheet variant="standard" headline="Scrolling body" top-divider bottom-divider open style="flex: 0 0 auto; --md-side-sheet-width: 280px; --md-side-sheet-max-width: 280px;">
    <p style="margin: 0; font-size: 14px;">Only the body scrolls — the header and the actions row stay pinned.</p>
    <p style="margin: 12px 0 0; font-size: 14px;">Keep adding copy and this region grows a scrollbar of its own.</p>
    <p style="margin: 12px 0 0; font-size: 14px;">Another paragraph, so the scroll is unmistakable.</p>
    <p style="margin: 12px 0 0; font-size: 14px;">And one more for good measure.</p>
    <p style="margin: 12px 0 0; font-size: 14px;">The dividers mark where the scroll region begins and ends.</p>
    <md-button slot="actions" variant="filled">Apply</md-button>
  </md-side-sheet>
  <div style="flex: 1; padding: 8px; color: var(--md-sys-color-on-surface-variant);">
    <p style="margin: 0;">Scroll inside the panel — the header, the dividers and the actions row do not move.</p>
  </div>
</div>
  • closeable defaults to true here — unlike md-bottom-sheet, where it defaults to false.
  • mdBack is emitted by the back affordance only; it does not navigate. You swap the panel’s content, update headline, and clear showBack yourself.
  • Only a modal sheet listens on the document. A standard sheet ignores Escape entirely, so one keypress can never collapse every inline panel on the page.
  • The body scrolls vertically when content overflows. Horizontal scrolling is an M3 violation — the panel is too narrow to show wide items.
  • Return focus to the trigger on close. A modal sheet restores the element that was focused when it opened; a standard sheet does not, so listen for mdClose if you drive one from a button.
Non-dismissible, no close button, and detached with actions Open in Storybook
Non-dismissible modal No close button Detached with actions

The scrim will not dismiss this sheet, and Escape is ignored — finish the choice below.

Cancel Export

No close glyph in the header — dismissal is the scrim, Escape, or your own control.

Done

A detached sheet floats inset from the edges with all four corners rounded.

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

<md-side-sheet variant="modal" headline="Confirm export" scrim-dismissible="false">
  <p>Neither the scrim nor Escape dismisses this sheet.</p>
  <md-button slot="actions" variant="text">Cancel</md-button>
  <md-button slot="actions" variant="filled">Export</md-button>
</md-side-sheet>

<md-side-sheet variant="modal" headline="Reading pane" closeable="false">
  <p>No close glyph in the header.</p>
</md-side-sheet>

<md-side-sheet variant="modal" detached headline="Order details" top-divider bottom-divider>
  <p>Floats inset from every edge.</p>
  <md-button slot="actions" variant="filled">Track</md-button>
</md-side-sheet>

<script type="module">
  var pairs = [['ss-nd-btn', 'ss-nd'], ['ss-ncb-btn', 'ss-ncb'], ['ss-det-btn', 'ss-det']];

  pairs.forEach(function (pair) {
    var btn = document.querySelector('#' + pair[0]);
    var sheet = document.querySelector('#' + pair[1]);
    btn.addEventListener('mdClick', function () { sheet.show(); });
    sheet.addEventListener('mdClose', function () { btn.focus({ preventScroll: true }); });
  });

  document.querySelectorAll('[data-ss2-close]').forEach(function (b) {
    b.addEventListener('mdClick', function () {
      document.querySelector('#' + b.getAttribute('data-ss2-close')).close();
    });
  });
</script>

The panel is a container, not a workflow — the workflow is yours. This one wires the whole loop: a trigger, a modal filter sheet, an actions row that applies the selection, and a summary written back into the page on mdClose.

A filter panel, wired end to end Open in Storybook
Filter results No filters applied.
Reset Apply
Show code for each technology
<md-button id="filter-btn" variant="filled" icon="filter_list">Filter results</md-button>
<span id="filter-summary">No filters applied.</span>

<md-side-sheet id="filters" variant="modal" side="end" headline="Filter results" top-divider bottom-divider>
<label><md-checkbox data-filter="In stock" checked></md-checkbox> In stock</label>
<label><md-checkbox data-filter="On sale"></md-checkbox> On sale</label>
<md-button slot="actions" variant="text" id="filter-reset">Reset</md-button>
<md-button slot="actions" variant="filled" id="filter-apply">Apply</md-button>
</md-side-sheet>

<script type="module">
const panel = document.getElementById('filters');
const trigger = document.getElementById('filter-btn');
const summary = document.getElementById('filter-summary');
const boxes = [...panel.querySelectorAll('[data-filter]')];

trigger.addEventListener('mdClick', () => panel.show());

document.getElementById('filter-reset').addEventListener('mdClick', () => {
  boxes.forEach((b) => (b.checked = false));
});

document.getElementById('filter-apply').addEventListener('mdClick', () => {
  const picked = boxes.filter((b) => b.checked).map((b) => b.dataset.filter);
  summary.textContent = picked.length ? picked.join(', ') : 'No filters applied.';
  panel.close();
});

panel.addEventListener('mdClose', () => trigger.focus({ preventScroll: true }));
</script>
EventCancelableDetailFires
mdOpennovoidThe sheet opens
mdClosenovoidThe sheet closes, by any route
mdCancelnovoidThe sheet is dismissedEscape, a scrim click, or the header close button
mdBacknovoidThe back affordance is pressed

mdCancel and mdClose are not interchangeable

Section titled “mdCancel and mdClose are not interchangeable”

mdCancel is dismissal only. mdClose fires on every close, including the one that immediately follows a dismissal and the one from your own close() call. Treat them as the same event and you will handle every dismissal twice.

You want to…Listen to
Restore focus to the triggermdClose
Persist or discard panel state on closemdClose
Warn that the user is abandoning unsaved editsmdCancel
Distinguish Apply from EscapemdCancel, and set a flag your mdClose handler reads
Swap the panel to its parent viewmdBack
Every event, logged as it fires Open in Storybook
Open the panel Clear log

The close glyph, Escape and the scrim each take a different route out. The back arrow stays put — it only reports the press, and the log shows what you would act on.

Done
Open the panel, then try each way out.
Show code for each technology
<md-button id="open-panel" variant="filled">Open the panel</md-button>

<md-side-sheet id="panel" variant="modal" headline="Advanced filters" show-back></md-side-sheet>

<script type="module">
const panel = document.getElementById('panel');
const trigger = document.getElementById('open-panel');

trigger.addEventListener('mdClick', () => panel.show());

panel.addEventListener('mdOpen', () => console.log('mdOpen'));

panel.addEventListener('mdBack', () => {   // you swap the content
  panel.headline = 'Filters';
  panel.showBack = false;
});

panel.addEventListener('mdCancel', () => console.log('dismissed, not applied'));
panel.addEventListener('mdClose', () => trigger.focus({ preventScroll: true }));
</script>

Nested flows are the one pattern the preview above cannot show end to end: the back arrow only tells you it was pressed, so re-rendering the panel’s body is yours to do. Here is that step, per technology.

<md-button id="open-panel" variant="filled">Filters</md-button>

<md-side-sheet id="panel" variant="modal" headline="Filters"></md-side-sheet>

<script type="module">
const panel = document.getElementById('panel');
const trigger = document.getElementById('open-panel');

const views = {
  root: { headline: 'Filters', back: false, body: '<p>Pick a category to drill in.</p>' },
  brand: { headline: 'Brand', back: true, body: '<p>Acme · Globex · Initech</p>' },
};

function render(name) {
  const view = views[name];
  panel.headline = view.headline;
  panel.showBack = view.back;
  panel.innerHTML = view.body;
}

trigger.addEventListener('mdClick', () => { render('root'); panel.show(); });

// mdBack does NOT navigate — it only reports the press.
panel.addEventListener('mdBack', () => render('root'));
panel.addEventListener('mdClose', () => trigger.focus({ preventScroll: true }));
</script>

Properties

PropertyAttributeTypeDefaultReflects
openopenbooleanfalseYes
variantvariant'standard' | 'modal''standard'Yes
sideside'start' | 'end''end'Yes
headlineheadlinestring''
closeablecloseablebooleantrueYes
showBackshow-backbooleanfalseYes
scrimDismissiblescrim-dismissiblebooleantrue
topDividertop-dividerbooleanfalseYes
bottomDividerbottom-dividerbooleanfalseYes
detacheddetachedbooleanfalseYes
sheetAriaLabelaria-labelstring''
densitydensity0 | -1 | -2 | -3 | -40Yes

Methods

MethodParameters
show()none
close()none

Slots

SlotDescription
(default)
backCustom back icon element (modal only)
headlineCustom headline content
closeCustom close element
actionsBottom action buttons

CSS Custom Properties

Override on the host element for per-instance theming:

PropertyDescription
--md-side-sheet-container-colorContainer background
--md-side-sheet-container-shapeContainer corner radius (detached)
--md-side-sheet-headline-colorHeadline text color
--md-side-sheet-content-colorContent text color
--md-side-sheet-scrim-colorModal scrim overlay color
--md-side-sheet-divider-colorDivider color
--md-side-sheet-widthSheet inline size
--md-side-sheet-max-widthMaximum inline size
--md-side-sheet-icon-colorClose / back icon color

CSS Shadow Parts

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

PartDescription
scrimModal scrim overlay
containerSheet surface
headerHeader row
headlineHeadline text
closeClose button wrapper
divider-topDivider between header and content
contentScrollable content area
divider-bottomDivider between content and actions
actionsBottom action bar
  • A modal sheet’s container is role="dialog" with aria-modal="true"; a standard sheet’s container is role="region". That choice is the whole accessibility contract, so pick it deliberately.
  • Name the sheet with headline (wired up as aria-labelledby) or aria-label. With neither, the container falls back to the label Side sheet — accurate, but useless to a screen-reader user. Never ship an unnamed panel.
  • A closed modal sheet stays in the DOM — it slides off-screen for the exit motion rather than being removed — and is marked inert, so its close button and slotted controls are dropped from the tab order and the a11y tree while hidden: no aria-hidden-focus violations. A closed standard sheet is display: none instead, which removes it from the layout and the a11y tree outright; it carries the same inert for consistency.
  • A modal sheet traps focus between two guard sentinels, moves focus to the first focusable element on open, and restores it to the opener on close. A standard sheet does neither: it joins the page tab order in DOM position, so put it where it belongs in the reading order.
  • Keep a keyboard path out: closeable (on by default), Escape, or a cancel button in the actions row.

Tab through the pair below. In the standard sheet you can tab straight past the panel back into the page; in the modal one, Tab cycles inside the panel until you press Escape, and focus lands back on the button that opened it.

Keyboard and screen-reader behaviour — standard vs modal Open in Storybook
standard

Not trapped — Tab moves on into the page.

Dismiss

role=region, no scrim, Escape ignored.

modal
Open the modal panel role=dialog, aria-modal, focus trapped, Escape closes.

Tab repeatedly — focus cycles between these controls and never leaves the panel.

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

<md-side-sheet variant="standard" headline="Related links" aria-label="Related links" open>
  <p>Not trapped — Tab moves on into the page.</p>
</md-side-sheet>

<md-side-sheet variant="modal" headline="Trapped focus" top-divider bottom-divider>
  <md-text-field label="Search" variant="outlined"></md-text-field>
  <md-button slot="actions" variant="filled">Apply</md-button>
</md-side-sheet>

<script type="module">
  var sheet = document.getElementById('ss-a11y');
  var btn = document.getElementById('ss-a11y-btn');

  btn.addEventListener('mdClick', function () { sheet.show(); });
  document.getElementById('ss-a11y-close').addEventListener('mdClick', function () { sheet.close(); });
</script>

The modal sheet restores focus itself, with preventScroll so the page does not jump. A standard sheet you drive from a button does not, which is why the demos above listen for mdClose and call focus() on the trigger.

RTLside="start" / side="end" and every padding are logical, so the same markup lands on the correct edge under dir="rtl" with no extra work:

<md-side-sheet variant="standard" side="end" headline="Filters"></md-side-sheet>
Same markup, dir=ltr vs dir=rtl Open in Storybook
ltr

Main content.

Trailing edge is the right.

rtl

المحتوى الرئيسي.

الحافة النهائية هي اليسار.

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;">ltr</span>
  <div dir="ltr" style="display:flex; gap:16px; align-items:stretch; min-block-size:150px; border:1px dashed var(--md-sys-color-outline-variant); border-radius:12px; padding:8px;">
    <div style="flex:1; padding:8px; color:var(--md-sys-color-on-surface-variant);"><p style="margin:0;">Main content.</p></div>
    <md-side-sheet variant="standard" side="end" headline="Filters" open style="flex:0 0 auto; --md-side-sheet-width:220px; --md-side-sheet-max-width:220px;">
      <p style="margin:0; font-size:14px;">Trailing edge is the right.</p>
    </md-side-sheet>
  </div>

  <span style="inline-size:3.5rem; opacity:.65; font-size:.75rem; font-family:ui-monospace,monospace;">rtl</span>
  <div dir="rtl" style="display:flex; gap:16px; align-items:stretch; min-block-size:150px; border:1px dashed var(--md-sys-color-outline-variant); border-radius:12px; padding:8px;">
    <div style="flex:1; padding:8px; color:var(--md-sys-color-on-surface-variant);"><p style="margin:0;">المحتوى الرئيسي.</p></div>
    <md-side-sheet variant="standard" side="end" headline="عوامل التصفية" open style="flex:0 0 auto; --md-side-sheet-width:220px; --md-side-sheet-max-width:220px;">
      <p style="margin:0; font-size:14px;">الحافة النهائية هي اليسار.</p>
    </md-side-sheet>
  </div>
</div>

There is no side="right". side="end" means the trailing edge, which is the right in LTR and the left in RTL — so if you reach for side="start" because a mock shows the panel on the left, it will jump to the right the moment the app is translated into Arabic or Hebrew. Both rows below are dir="rtl":

side is logical — the same value moves edges under dir=rtl Open in Storybook
do

side=end — supplementary content on the trailing edge, wherever that is.

تتبع اتجاه النص.

don't

هنا لأن التصميم قال يسار.

side=start picked to mean the left — under RTL it is now the right, and the panel has swapped sides.

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;">do</span>
  <div dir="rtl" style="display:flex; gap:16px; align-items:stretch; min-block-size:150px; border:1px dashed var(--md-sys-color-outline-variant); border-radius:12px; padding:8px;">
    <div style="flex:1; padding:8px; color:var(--md-sys-color-on-surface-variant);"><p style="margin:0; font-size:14px;">side=end — supplementary content on the trailing edge, wherever that is.</p></div>
    <md-side-sheet variant="standard" side="end" headline="عوامل التصفية" open style="flex:0 0 auto; --md-side-sheet-width:210px; --md-side-sheet-max-width:210px;">
      <p style="margin:0; font-size:14px;">تتبع اتجاه النص.</p>
    </md-side-sheet>
  </div>

  <span style="inline-size:3.5rem; opacity:.65; font-size:.75rem; font-family:ui-monospace,monospace;">don't</span>
  <div dir="rtl" style="display:flex; gap:16px; align-items:stretch; min-block-size:150px; border:1px dashed var(--md-sys-color-error); border-radius:12px; padding:8px;">
    <md-side-sheet variant="standard" side="start" headline="عوامل التصفية" open style="flex:0 0 auto; --md-side-sheet-width:210px; --md-side-sheet-max-width:210px;">
      <p style="margin:0; font-size:14px;">هنا لأن التصميم قال يسار.</p>
    </md-side-sheet>
    <div style="flex:1; padding:8px; color:var(--md-sys-color-on-surface-variant);"><p style="margin:0; font-size:14px;">side=start picked to mean the left — under RTL it is now the right, and the panel has swapped sides.</p></div>
  </div>
</div>

density="-1…-4" compacts the header, the content padding and the actions row, and overrides the rung inherited from a global data-density ancestor. The 0 row below is the untouched default — not a rule of its own.

Density 0 through -4 — header, padding and actions all taper
0

Default padding.

Reset
-1

One rung tighter.

Reset
-2

Admin density.

Reset
-3

Dense dashboards.

Reset
-4

The floor.

Reset
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-side-sheet variant="standard" headline="Filters" density="0" open style="--md-side-sheet-width:260px; --md-side-sheet-max-width:260px;">
    <p style="margin:0; font-size:13px;">Default padding.</p>
    <md-button slot="actions" variant="text">Reset</md-button>
  </md-side-sheet>

  <span style="inline-size:3.5rem; opacity:.65; font-size:.75rem; font-family:ui-monospace,monospace;">-1</span>
  <md-side-sheet variant="standard" headline="Filters" density="-1" open style="--md-side-sheet-width:260px; --md-side-sheet-max-width:260px;">
    <p style="margin:0; font-size:13px;">One rung tighter.</p>
    <md-button slot="actions" variant="text">Reset</md-button>
  </md-side-sheet>

  <span style="inline-size:3.5rem; opacity:.65; font-size:.75rem; font-family:ui-monospace,monospace;">-2</span>
  <md-side-sheet variant="standard" headline="Filters" density="-2" open style="--md-side-sheet-width:260px; --md-side-sheet-max-width:260px;">
    <p style="margin:0; font-size:13px;">Admin density.</p>
    <md-button slot="actions" variant="text">Reset</md-button>
  </md-side-sheet>

  <span style="inline-size:3.5rem; opacity:.65; font-size:.75rem; font-family:ui-monospace,monospace;">-3</span>
  <md-side-sheet variant="standard" headline="Filters" density="-3" open style="--md-side-sheet-width:260px; --md-side-sheet-max-width:260px;">
    <p style="margin:0; font-size:13px;">Dense dashboards.</p>
    <md-button slot="actions" variant="text">Reset</md-button>
  </md-side-sheet>

  <span style="inline-size:3.5rem; opacity:.65; font-size:.75rem; font-family:ui-monospace,monospace;">-4</span>
  <md-side-sheet variant="standard" headline="Filters" density="-4" open style="--md-side-sheet-width:260px; --md-side-sheet-max-width:260px;">
    <p style="margin:0; font-size:13px;">The floor.</p>
    <md-button slot="actions" variant="text">Reset</md-button>
  </md-side-sheet>
</div>

Measured across the five: the header goes 76 → 69 → 62 → 59 → 56px and the content padding 12/24 → 11/22 → 10/20 → 9/18 → 8/16px.

They compose: a data-density ancestor sets the rung for everything below it, dir="rtl" flips the edge, and a local density="-1…-4" prop tightens one panel further. Going back the other way needs the custom property, not density="0" — the third panel below resets with style="--md-sys-density-scale: 0".

dir=rtl with data-density=-2 — one panel inherits, one tightens to -4, one resets via --md-sys-density-scale

dir=rtl, data-density=-2 on the wrapper.

يرث الدرجة ‎-2‎ من الأصل.

إعادة

‎density="-4"‎ يضغط أكثر من الأصل.

إعادة

‎--md-sys-density-scale: 0‎ يعيد الضبط.

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

<div dir="rtl" data-density="-2" style="display:flex; gap:16px; align-items:stretch; min-block-size:180px; border:1px dashed var(--md-sys-color-outline); border-radius:12px; padding:12px;">
  <div style="flex:1; min-inline-size:6rem; padding:8px; color:var(--md-sys-color-on-surface-variant);">
    <p style="margin:0; font-size:14px;">dir=rtl, data-density=-2 on the wrapper.</p>
  </div>
  <md-side-sheet variant="standard" side="end" headline="موروث" open style="flex:0 0 auto; --md-side-sheet-width:190px; --md-side-sheet-max-width:190px;">
    <p style="margin:0; font-size:13px;">يرث الدرجة ‎-2‎ من الأصل.</p>
    <md-button slot="actions" variant="text">إعادة</md-button>
  </md-side-sheet>
  <md-side-sheet variant="standard" side="end" headline="مضغوط" density="-4" open style="flex:0 0 auto; --md-side-sheet-width:190px; --md-side-sheet-max-width:190px;">
    <p style="margin:0; font-size:13px;">‎density="-4"‎ يضغط أكثر من الأصل.</p>
    <md-button slot="actions" variant="text">إعادة</md-button>
  </md-side-sheet>
  <md-side-sheet variant="standard" side="end" headline="عادي" open style="flex:0 0 auto; --md-sys-density-scale:0; --md-side-sheet-width:190px; --md-side-sheet-max-width:190px;">
    <p style="margin:0; font-size:13px;">‎--md-sys-density-scale: 0‎ يعيد الضبط.</p>
    <md-button slot="actions" variant="text">إعادة</md-button>
  </md-side-sheet>
</div>

Longer translations in a fixed-width panel

Section titled “Longer translations in a fixed-width panel”

The panel’s width does not grow with its content, so a headline or action label that fits in English can wrap — or ellipsize, since the headline is a single truncating line. Check each locale at your narrowest rung.

Translated panels — German and Japanese Open in Storybook

Längere Übersetzungen können in einem schmalen Panel umbrechen — prüfen Sie die Breite pro Sprache.

Zurücksetzen Anwenden

見出し、本文、操作ラベルをすべて翻訳します。

リセット 適用
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: 16px; align-items: stretch; min-block-size: 200px;">
  <md-side-sheet variant="standard" headline="Filter" open style="flex: 0 0 auto; --md-side-sheet-width: 240px; --md-side-sheet-max-width: 240px;">
    <p style="margin: 0; font-size: 14px;">Längere Übersetzungen können in einem schmalen Panel umbrechen — prüfen Sie die Breite pro Sprache.</p>
    <md-button slot="actions" variant="text">Zurücksetzen</md-button>
    <md-button slot="actions" variant="filled">Anwenden</md-button>
  </md-side-sheet>
  <md-side-sheet variant="standard" headline="フィルター" open style="flex: 0 0 auto; --md-side-sheet-width: 240px; --md-side-sheet-max-width: 240px;">
    <p style="margin: 0; font-size: 14px;">見出し、本文、操作ラベルをすべて翻訳します。</p>
    <md-button slot="actions" variant="text">リセット</md-button>
    <md-button slot="actions" variant="filled">適用</md-button>
  </md-side-sheet>
</div>

Density — set it globally with data-density on any ancestor, or locally with the density="-1…-4" prop, which wins over the inherited rung. density="0" is not defined, so it inherits rather than resets; use --md-sys-density-scale: 0 for that. See Density.

i18n — translate headline, aria-label and every action label. The panel width is fixed-ish, so check that longer translations do not wrap awkwardly.

Custom propertyPurposeDefault
--md-side-sheet-container-colorSurface background — variant="standard" only--md-sys-color-surface (a modal container is fixed at --md-sys-color-surface-container-low and ignores this property)
--md-side-sheet-container-shapeCorner radius0px, or --md-sys-shape-corner-large when detached
--md-side-sheet-headline-colorHeadline text--md-sys-color-on-surface-variant
--md-side-sheet-content-colorBody text--md-sys-color-on-surface
--md-side-sheet-scrim-colorModal backdroprgba(0, 0, 0, 0.32)
--md-side-sheet-divider-colorTop / bottom rules--md-sys-color-outline-variant
--md-side-sheet-icon-colorClose and back glyphs--md-sys-color-on-surface-variant
--md-side-sheet-widthPanel inline sizemax(280px, 360px + rung × 16px)
--md-side-sheet-max-widthMaximum inline sizemax(320px, 400px + rung × 16px)
Themed instances Open in Storybook
Toggle themed panel

Narrower panel, primary-tinted headline and divider.

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

<md-side-sheet
  variant="standard"
  side="end"
  headline="Themed"
  top-divider
  open
  style="
  --md-side-sheet-container-color: var(--md-sys-color-surface-container-highest);
  --md-side-sheet-headline-color: var(--md-sys-color-primary);
  --md-side-sheet-divider-color: var(--md-sys-color-primary);
  --md-side-sheet-width: 240px;
  --md-side-sheet-max-width: 240px;
  "
  >
  <p>Narrower panel, primary-tinted headline and divider.</p>
</md-side-sheet>

<script type="module">
  var sheet = document.getElementById('ss-theme');
  var toggle = document.getElementById('ss-theme-toggle');

  toggle.addEventListener('mdClick', function () {
    if (sheet.open) sheet.close();
    else sheet.show();
  });
  sheet.addEventListener('mdClose', function () { toggle.focus({ preventScroll: true }); });
</script>

Check any override in both themes — the sheet sits on a surface role that is much closer to the page background on dark, so a panel that reads as clearly separated on light can lose its edge.

Pinned to the dark theme Open in Storybook

Pinned to the dark palette regardless of the theme you are reading in.

Surface, divider and text roles all resolve from the dark palette.

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

<div data-theme="dark" style="display: flex; gap: 16px; align-items: stretch; min-block-size: 200px; padding: 12px; border-radius: 12px; background: var(--md-sys-color-surface);">
  <div style="flex: 1; padding: 8px; color: var(--md-sys-color-on-surface-variant);">
    <p style="margin: 0;">Pinned to the dark palette regardless of the theme you are reading in.</p>
  </div>
  <md-side-sheet variant="standard" headline="Filters" open style="flex: 0 0 auto; --md-side-sheet-width: 240px; --md-side-sheet-max-width: 240px;">
    <p style="margin: 0; font-size: 14px;">Surface, divider and text roles all resolve from the dark palette.</p>
    <md-button slot="actions" variant="filled">Apply</md-button>
  </md-side-sheet>
</div>

CSS partsscrim, container, header, headline, close, divider-top, content, divider-bottom, actions. Reach for them when a custom property is not enough: an accent border on the container, a different headline treatment, a wider gap between actions.

CSS parts and custom-property overrides Open in Storybook

Main content — the panel beside it is styled through its parts.

An accent edge on the container, an italic headline and a wider action gap.

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

<style>
  .ss-parts::part(container) { border-inline-start: 3px solid var(--md-sys-color-primary); }
  .ss-parts::part(headline) { font-style: italic; letter-spacing: .04em; }
  .ss-parts::part(actions) { gap: 16px; }
</style>
<div style="display: flex; gap: 16px; align-items: stretch; min-block-size: 220px;">
  <div style="flex: 1; padding: 8px; color: var(--md-sys-color-on-surface-variant);">
    <p style="margin: 0;">Main content — the panel beside it is styled through its parts.</p>
  </div>
  <md-side-sheet class="ss-parts" variant="standard" headline="Styled with ::part()" open style="flex: 0 0 auto; --md-side-sheet-width: 260px; --md-side-sheet-max-width: 260px;">
    <p style="margin: 0; font-size: 14px;">An accent edge on the container, an italic headline and a wider action gap.</p>
    <md-button slot="actions" variant="text">Cancel</md-button>
    <md-button slot="actions" variant="filled">Apply</md-button>
  </md-side-sheet>
</div>
md-side-sheet::part(container) {
border-inline-start: 3px solid var(--md-sys-color-primary);
}
md-side-sheet::part(content) {
padding-inline: 24px;
}

md-bottom-sheet · md-dialog · md-navigation-rail · md-navigation-bar · md-menu · md-card · md-icon-button

For AI Agents — md-side-sheet

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-side-sheet 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-side-sheet readme.md

# md-side-sheet

<!-- llm:meta
tag: md-side-sheet
category: containment
status: md3-mapped
m3-guidelines: https://m3.material.io/components/side-sheets/guidelines
form-associated: false
depends-on: md-icon-button
used-by: none
-->

**Supplementary content along the side of the screen.** Standard (an inline,
non-modal region that sits beside your content) or modal (a fixed overlay with
a scrim and a focus trap), anchored to either logical edge, with an optional
back affordance for nested flows.

> 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

- **Desktop / large-screen** supplementary content: filters, details, help, a
  properties panel.
- Content the user consults **alongside** the main view
  (`variant="standard"`).
- A focused side task over the content (`variant="modal"`).
- Nested navigation within the panel (`show-back`, modal only).

## When NOT to use

| Situation | Use instead |
|---|---|
| Mobile supplementary content | `md-bottom-sheet` |
| A blocking decision | `md-dialog` |
| Brief feedback | `md-snackbar` |
| App-level destination navigation | `md-navigation-rail` / `md-navigation-bar` |
| A short action list | `md-menu` |
| Primary content | A page |

## Decision cues

| Need | Setting |
|---|---|
| Sits beside content, no scrim, no focus trap | `variant="standard"` (default) |
| Over the content with a scrim and a focus trap | `variant="modal"` |
| Trailing edge (M3's usual choice) | `side="end"` (default) |
| Leading edge | `side="start"` |
| Floating, rounded, inset from the edge | `detached` |
| Back arrow for a nested view | `show-back` (modal only) + `mdBack` |
| No close button | `closeable="false"` |
| Your own close control | `slot="close"` (keep `closeable`) |
| Prevent click-away **and** Escape (modal) | `scrim-dismissible="false"` |
| Rule under the header | `top-divider` |
| Rule above the actions row | `bottom-divider` (needs `slot="actions"` content) |
| Open/close from code | `show()` / `close()`, or set `open` |

## API contract

```html
<md-side-sheet
  open                                 <!-- default: false; reflects -->
  variant="standard|modal"             <!-- default: standard -->
  side="start|end"                     <!-- default: end (logical) -->
  headline="Filters"                   <!-- default: "" -->
  closeable="true|false"               <!-- default: true -->
  show-back                            <!-- default: false (modal only) -->
  scrim-dismissible="true|false"       <!-- default: true (modal only) -->
  top-divider                          <!-- default: false -->
  bottom-divider                       <!-- default: false; needs slotted actions -->
  detached                             <!-- default: false -->
  aria-label="Filter panel"            <!-- default: "" -->
  density="-1|-2|-3|-4"                <!-- default: 0 (uncompacted; only -1…-4 have rules) -->
>
  <label><md-checkbox value="in-stock"></md-checkbox> In stock</label>
  <md-button slot="actions" variant="text">Reset</md-button>
  <md-button slot="actions" variant="filled">Apply</md-button>
</md-side-sheet>
```

**Events** — `mdOpen`, `mdClose`, `mdCancel`, `mdBack`, all `CustomEvent<void>`
and all default Stencil events (bubbling and composed).

**Methods** — `show(): Promise<void>` and `close(): Promise<void>`. Both just
set `open`, so `open` stays the single source of truth.

**Slots** — `(default)` main content · `headline` · `back` (rendered only when
`variant="modal"` and `show-back`) · `close` (rendered only when `closeable`) ·
`actions`.

**Parts** — `scrim` (modal only), `container`, `header`, `headline`, `close`,
`divider-top`, `content`, `divider-bottom`, `actions`.

### Behavioral contract worth knowing

- **`closeable` defaults to `true`** here — the opposite of `md-bottom-sheet`.
- **The two variants have different accessibility contracts.**
  `variant="standard"` renders `role="region"`, no scrim, no focus trap, no
  body-scroll lock, and no document-level Escape handler — the rest of the page
  stays fully interactive. `variant="modal"` renders `role="dialog"` +
  `aria-modal="true"`, a scrim, a focus trap, a body-scroll lock and Escape
  handling. Choose deliberately.
- **`scrim-dismissible="false"` also disables Escape** on a modal sheet — the
  Escape handler returns early after swallowing the key. Combine it with
  `closeable` or an actions-row cancel, or keyboard users have no way out.
- Escape on an open modal sheet is handled by a **capture-phase listener on
  `document`** with `preventDefault()` and `stopPropagation()`, so an outer
  Escape handler will not also fire.
- **`mdOpen` fires on mount for a sheet rendered with `open`** (the mount
  handler runs the open path), and again on every later change. `mdClose` fires
  on every close.
- `mdCancel` fires only on dismissal — a scrim click, Escape, or the built-in
  close button. `mdClose` fires on *every* close, including those, so a
  dismissal emits `mdCancel` **and** `mdClose`.
- **`mdBack` does not navigate and does not close the sheet.** It only reports
  the press; you swap the panel's content and usually clear `show-back`.
- **Slotted `close` / `back` / action elements are not wired.** Only the
  built-in `md-icon-button` fallbacks call `close()` / emit `mdBack`; your own
  controls must do it themselves.
- **`bottom-divider` is double-gated.** The bottom rule renders only when the
  flag is set **and** there is slotted `[slot="actions"]` content — the actions
  row is what it separates. `<md-side-sheet bottom-divider>` with no actions
  renders no rule at all. `top-divider` has no such gate.
- `side` is **logical**: `end` is the right edge in LTR and the left edge in
  RTL. There is no `left` / `right` value.
- A closed **standard** sheet is `display: none` — it occupies no layout space.
  A closed **modal** sheet stays in the DOM, slid off-screen, with the container
  `inert` and `aria-hidden="true"`.
- **Focus is handled for you on the modal variant only.** It focuses the first
  focusable element on open, keeps focus inside via sentinel guards plus a
  `focusin` listener, and restores focus to the previously focused element (with
  `preventScroll`) on close. The standard variant does none of this — its
  content simply joins the page tab order.
- **The header row always renders**, even with no `headline`, no `closeable`
  and no back button.
- `aria-labelledby` is wired only when the `headline` **prop** is non-empty. A
  headline supplied purely through `slot="headline"` shows visually but does not
  name the sheet — set `aria-label` (or the prop) as well.
- The default panel width is `max(280px, 360px + density × 16px)`, capped at
  `max(320px, 400px + density × 16px)` and never wider than the viewport (a
  modal sheet goes full-width on narrow screens rather than overflowing).
- The body scrolls vertically when the content overflows.

---

## Do / Don't

Sourced from [M3 · Side sheets · Guidelines](https://m3.material.io/components/side-sheets/guidelines).

| ✅ Do | ❌ Don't |
|---|---|
| Place the sheet along the screen edge — usually the trailing side, to stay clear of leading-edge navigation; a slight 16px inset is fine | Don't inset it far beyond that margin — it makes position and scroll behaviour unclear and obscures primary content |
| Let it scroll vertically when content exceeds the screen height | Don't allow horizontal scrolling, or lay it out so it looks horizontally scrollable — the narrow width can't show wide items |
| Use `standard` when the user works with the sheet and the content together | Don't trap focus in a non-modal panel |
| Use `modal` when the side task should take over | Don't leave a modal sheet without a keyboard exit |
| Keep the panel narrow and its content single-column | Don't build a wide multi-column layout inside it |
| Give it a `headline` or `aria-label` | Don't ship a panel named only by the built-in English fallback |
| Use `show-back` for genuinely nested views | Don't show a back arrow that goes nowhere |

---

## Patterns

```html
<!-- Standard: filters that sit beside the results, page stays interactive -->
<div style="display: flex; block-size: 100vh;">
  <main style="flex: 1;">…results…</main>

  <md-side-sheet id="filters" open variant="standard" side="end"
                 headline="Filters" top-divider bottom-divider>
    <label><md-checkbox value="in-stock"></md-checkbox> In stock</label>
    <label><md-checkbox value="on-sale"></md-checkbox> On sale</label>
    <md-button slot="actions" variant="text" id="filters-reset">Reset</md-button>
    <md-button slot="actions" variant="filled" id="filters-apply">Apply</md-button>
  </md-side-sheet>
</div>

<script type="module">
  const sheet = document.getElementById('filters');
  // Slotted action buttons never close the sheet on their own.
  document.getElementById('filters-apply')
    .addEventListener('mdClick', () => sheet.close());
</script>
```

```html
<!-- Modal: a focused side task over the content -->
<md-button id="open-details">Show details</md-button>

<md-side-sheet id="details" variant="modal" headline="Details" detached>
  <p>Order #1043, placed 3 March.</p>
</md-side-sheet>

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

  document.getElementById('open-details')
    .addEventListener('mdClick', () => sheet.show());

  // Focus is restored automatically on close — no extra code needed.
  sheet.addEventListener('mdCancel', () => console.log('dismissed'));
</script>
```

```html
<!-- Nested view: mdBack reports the press, you swap the content -->
<md-side-sheet id="panel" variant="modal" headline="Filters"></md-side-sheet>

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

  function showAdvanced() {
    panel.headline = 'Advanced filters';
    panel.showBack = true;
  }

  panel.addEventListener('mdBack', () => {
    panel.headline = 'Filters';
    panel.showBack = false;
  });

  panel.show();
  showAdvanced();
</script>
```

```html
<!-- Custom, localized close button: keep `closeable` and wire it yourself -->
<md-side-sheet id="ayarlar" variant="modal" headline="Ayarlar">
  <md-icon-button slot="close" icon="close" aria-label="Kapat"
                  id="ayarlar-close"></md-icon-button>
  <p>Tercihleriniz.</p>
</md-side-sheet>

<script type="module">
  const sheet = document.getElementById('ayarlar');
  document.getElementById('ayarlar-close')
    .addEventListener('mdClick', () => sheet.close());
</script>
```

## Anti-patterns

| ❌ Wrong | ✅ Right | Why |
|---|---|---|
| `scrim-dismissible="false"` as the only change on a modal sheet | Keep `closeable` or add an actions-row cancel | It disables Escape too, so keyboard users get stuck. |
| Expecting `mdBack` to navigate or close | Swap the content yourself | It only reports the press. |
| Expecting a slotted `close` / `back` element to work | Wire it to `close()` / your handler | Only the built-in fallbacks are wired. |
| `show-back` on a `standard` sheet | Use `variant="modal"` | The back button renders only for the modal variant. |
| Focus-trapping or scroll-locking a `standard` sheet yourself | Use `modal` if you need that | Standard is a non-modal `role="region"` by design. |
| Writing restore-focus code around a modal sheet | Let it restore focus | It saves and restores the previous focus itself. |
| Assuming `mdOpen` won't fire for a sheet rendered with `open` | Expect it on mount | The mount handler runs the open path. |
| Handling `mdCancel` and `mdClose` as mutually exclusive | `mdCancel` implies `mdClose` | A dismissal emits both — you'll double-handle. |
| `side="right"` / `side="left"` | `side="end"` / `side="start"` | The prop is logical, not physical. |
| Assuming `closeable` defaults to `false` | It defaults to **true** here | Differs from `md-bottom-sheet`. |
| `--md-side-sheet-container-color` on a modal sheet | Restyle via `::part(container)` | The modal container's background is not driven by that property. |
| `<span slot="headline">` as the only name | Also set `headline` or `aria-label` | `aria-labelledby` is wired only from the prop. |
| A wide, multi-column side sheet | Keep it narrow, single column | M3 explicit rule. |
| Horizontal scrolling inside | Vertical only | M3 explicit rule. |
| Deeply insetting the sheet from the edge | ~16px at most (`detached`) | M3 explicit rule. |
| A side sheet on mobile | `md-bottom-sheet` | Wrong surface for narrow viewports. |

## Accessibility, RTL, density, i18n

**Accessibility**
- `variant="standard"` is a non-modal `role="region"`: the rest of the page
  stays reachable, focus is **not** trapped, and there is no Escape handler.
  `variant="modal"` is `role="dialog"` with `aria-modal="true"`: focus moves
  into it on open, is kept inside, and returns to the opener on close.
- Name the sheet with the `headline` prop (wired to `aria-labelledby`) or the
  `aria-label` attribute. With neither, it falls back to the hard-coded English
  string "Side sheet".
- Keep a keyboard path out of a modal sheet: `closeable` (on by default), an
  actions-row cancel, or leave `scrim-dismissible` on so Escape works.
- The built-in close and back buttons carry hard-coded English names
  ("Close side sheet", "Back"). Slot your own `md-icon-button` with a localized
  `aria-label` — and wire its handler — to translate them.
- While a modal sheet is closed its container is `inert`, so nothing inside is
  reachable by keyboard or exposed to assistive tech.
- For a `standard` sheet, place it in the DOM where it belongs in the reading
  order — nothing moves it for you.

**RTL** — `side="start"` / `side="end"`, the adjacent-edge border and all
padding are logical, so the sheet lands on the correct edge under `dir="rtl"`
with no extra work. Swap a directional glyph if you slot your own back icon.

**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 a sheet out of an inherited
rung; use `style="--md-sys-density-scale: 0"` to reset the scale locally.
Density narrows the panel and compacts the header, icon buttons, content and
actions row.

**i18n** — translate `headline`, `aria-label`, action labels and body content,
and replace the built-in close/back buttons via `slot="close"` / `slot="back"`
when you need their labels translated. The panel is narrow — check that longer
translations don't wrap awkwardly in the header.

## Related components

`md-bottom-sheet` · `md-dialog` · `md-navigation-rail` · `md-menu` ·
`md-card` · `md-icon-button`

## Theming

| Custom property | Purpose | Default |
|---|---|---|
| `--md-side-sheet-container-color` | Surface background — **`standard` variant only** | `--md-sys-color-surface` |
| `--md-side-sheet-container-shape` | Corner radius; applied only when `detached` | `0px`, becoming `--md-sys-shape-corner-large` under `detached` |
| `--md-side-sheet-headline-color` | Headline text colour | `--md-sys-color-on-surface-variant` |
| `--md-side-sheet-content-color` | Body text colour | `--md-sys-color-on-surface` |
| `--md-side-sheet-scrim-color` | Modal backdrop colour | `rgba(0, 0, 0, 0.32)` |
| `--md-side-sheet-divider-color` | Top / bottom rules and the standard variant's edge border | `--md-sys-color-outline-variant` |
| `--md-side-sheet-icon-color` | Close / back glyph colour | `--md-sys-color-on-surface-variant` |
| `--md-side-sheet-width` | Panel inline size | `max(280px, 360px + density × 16px)` |
| `--md-side-sheet-max-width` | Panel maximum inline size | `max(320px, 400px + density × 16px)` |

The modal container's background is `--md-sys-color-surface-container-low` and
is not driven by `--md-side-sheet-container-color`; restyle it through
`::part(container)`.

**CSS parts** — `scrim` (modal only), `container`, `header`, `headline`,
`close`, `divider-top`, `content`, `divider-bottom`, `actions`.

```css
md-side-sheet.inspector {
  --md-side-sheet-width: 420px;
  --md-side-sheet-max-width: 480px;
  --md-side-sheet-divider-color: var(--md-sys-color-outline);
}

md-side-sheet.inspector[variant='modal']::part(container) {
  background-color: var(--md-sys-color-surface-container-highest);
}
```

<!-- Auto Generated Below -->


## Overview

MD3 Side sheet — anchored panel for secondary content and actions.

## Properties

| Property           | Attribute           | Description                                                                                                                                                                                           | Type                        | Default      |
| ------------------ | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- | ------------ |
| `bottomDivider`    | `bottom-divider`    | Show divider between content and actions                                                                                                                                                              | `boolean`                   | `false`      |
| `closeable`        | `closeable`         | Show close icon button                                                                                                                                                                                | `boolean`                   | `true`       |
| `density`          | `density`           | Local density rung. Drives the same `--md-sys-density-scale` signal that a global `data-density` ancestor sets, so a local value simply overrides the inherited one. 0 = default, -4 = ultra-compact. | `-1 \| -2 \| -3 \| -4 \| 0` | `0`          |
| `detached`         | `detached`          | Detached style with rounded corners and margin                                                                                                                                                        | `boolean`                   | `false`      |
| `headline`         | `headline`          | Headline text (or use the headline slot)                                                                                                                                                              | `string`                    | `''`         |
| `open`             | `open`              | Whether the sheet is visible                                                                                                                                                                          | `boolean`                   | `false`      |
| `scrimDismissible` | `scrim-dismissible` | Whether clicking the scrim closes the sheet (modal only)                                                                                                                                              | `boolean`                   | `true`       |
| `sheetAriaLabel`   | `aria-label`        | Custom aria-label for the container                                                                                                                                                                   | `string`                    | `''`         |
| `showBack`         | `show-back`         | Show optional back navigation icon (modal only)                                                                                                                                                       | `boolean`                   | `false`      |
| `side`             | `side`              | Which edge the sheet is anchored to                                                                                                                                                                   | `"end" \| "start"`          | `'end'`      |
| `topDivider`       | `top-divider`       | Show divider between header and content                                                                                                                                                               | `boolean`                   | `false`      |
| `variant`          | `variant`           | `standard` coexists with page content; `modal` overlays with scrim                                                                                                                                    | `"modal" \| "standard"`     | `'standard'` |


## Events

| Event      | Description                                        | Type                |
| ---------- | -------------------------------------------------- | ------------------- |
| `mdBack`   | Emits when the back button is pressed (modal only) | `CustomEvent<void>` |
| `mdCancel` | Emits when dismissed via scrim click or Escape     | `CustomEvent<void>` |
| `mdClose`  | Emits when the sheet closes                        | `CustomEvent<void>` |
| `mdOpen`   | Emits when the sheet opens                         | `CustomEvent<void>` |


## Methods

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



#### Returns

Type: `Promise<void>`



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



#### Returns

Type: `Promise<void>`




## Slots

| Slot         | Description                                               |
| ------------ | --------------------------------------------------------- |
|              | Main content                                              |
| `"actions"`  | Bottom action buttons (optional)                          |
| `"back"`     | Custom back icon element (modal only)                     |
| `"close"`    | Custom close element (replaces default close icon button) |
| `"headline"` | Custom headline content                                   |


## Shadow Parts

| Part               | Description                                  |
| ------------------ | -------------------------------------------- |
| `"actions"`        | Bottom action bar                            |
| `"close"`          | Close button wrapper                         |
| `"container"`      | Sheet surface                                |
| `"content"`        | Scrollable content area                      |
| `"divider-bottom"` | Bottom divider (between content and actions) |
| `"divider-top"`    | Top divider (between header and content)     |
| `"header"`         | Header row (headline + close)                |
| `"headline"`       | Headline text                                |
| `"scrim"`          | Modal scrim overlay                          |


## Dependencies

### Depends on

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

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

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

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

AWC UI Operator’s Manual

# main-llm.md — AWC UI build director

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

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

Your job, in order:

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

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

---

## §1 — Interview the user

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

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

### 1.1 Scope and shape

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

### 1.2 Look and feel

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

### 1.3 Internationalization

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

### 1.4 Data and forms

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

### 1.5 Constraints

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

---

## §2 — Map answers to configuration

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

### 2.1 Global switches — the complete set

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

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

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

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

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

---

## §3 — Install and bootstrap

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

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

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

defineCustomElements(window);
```

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

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

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

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

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

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

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

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

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

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

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

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

---

## §4 — Global configuration reference

### 4.1 The token system

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

#### Color roles

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

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

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

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

#### Shape tokens

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

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

#### Elevation tokens

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

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

#### Motion tokens

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

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

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

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

#### State-layer opacities

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

#### Typography, spacing and layering

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

### 4.2 Theming and rebranding

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

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

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

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

### 4.3 Density

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

Two details that bite:

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

### 4.4 Dark mode

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

### 4.5 RTL

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

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

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

### 4.6 Internationalization

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

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

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

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

### 4.7 Forms and validation

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

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

Rules:

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

---

## §5 — Component decision matrix

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

### 5.1 Actions

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

### 5.2 Text input

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

### 5.3 Selection

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

### 5.4 Navigation

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

### 5.5 Containment and feedback

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

### 5.6 Data

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

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

### 5.7 Choosing the variant

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

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

### 5.8 Not in the library

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

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

---

## §6 — Component inventory

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

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

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

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

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

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

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

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

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

---

## §7 — Composition rules

### 7.1 What nests inside what

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

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

### 7.2 Pairs that belong together

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

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

### 7.3 Nesting that is always wrong

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

---

## §8 — Page recipes

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

### 8.1 Login screen

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

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

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

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

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

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

### 8.2 Settings page (mobile)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

### 8.4 Dashboard with a FAB

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

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

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

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

### 8.5 Destructive confirmation dialog

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

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

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

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

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

### 8.6 Tabbed content page

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

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

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

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

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

### 8.7 The full recipe library

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

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

---

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

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

### 9.1 API and styling

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

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

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

### 9.2 Content and hierarchy

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

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

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

### 9.3 Accessibility contract

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

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

---

## §10 — Before you ship

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