Skip to content

Card

A container for related content and actions about a single subject. Three elevations, an optional interactive (clickable) mode with ripple, and optional drag reporting. The host is a display: flex; flex-direction: column box with its own padding and gap — you compose the inside freely.

Live preview Open in Storybook

Elevated

The default. A shadow separates it from the background.

Filled

A flat tonal surface, no shadow.

Outlined

A defined edge, no shadow.

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

<md-card variant="elevated" style="inline-size: 220px;">
  <h3 style="margin: 0; font-size: 16px;">Elevated</h3>
  <p style="margin: 0; font-size: 14px;">The default. A shadow separates it from the background.</p>
</md-card>
<md-card variant="filled" style="inline-size: 220px;">
  <h3 style="margin: 0; font-size: 16px;">Filled</h3>
  <p style="margin: 0; font-size: 14px;">A flat tonal surface, no shadow.</p>
</md-card>
<md-card variant="outlined" style="inline-size: 220px;">
  <h3 style="margin: 0; font-size: 16px;">Outlined</h3>
  <p style="margin: 0; font-size: 14px;">A defined edge, no shadow.</p>
</md-card>

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


<md-card>Card content</md-card>
  • Grouping content and actions about one subject — a product, an article, a contact — especially when several appear together as a collection.
  • An entry point into a detail view (interactive).
  • Reorderable tiles (drag-enabled).
SituationUse instead
A vertical list of recordsmd-list + md-list-item
Tabular, comparable datamd-table
A blocking decisionmd-dialog
A transient messagemd-snackbar
Content that spacing or a heading would organise betterNothing — M3 warns against forcing content into cards
Collapsible sectionsmd-accordion
A single actionmd-button
VariantEmphasisUse for
elevatedShadowDefault. Separation from a plain background
filledTonal fillA flat, grouped surface with no shadow
outlinedBorderA defined edge where shadows would be noisy
All three variants Open in Storybook

Weekly report

Generated 2 hours ago.

Weekly report

Generated 2 hours ago.

Weekly report

Generated 2 hours ago.

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

<md-card variant="elevated" style="inline-size: 200px;">
  <h3 style="margin: 0; font-size: 16px;">Weekly report</h3>
  <p style="margin: 0; font-size: 14px;">Generated 2 hours ago.</p>
</md-card>
<md-card variant="filled" style="inline-size: 200px;">
  <h3 style="margin: 0; font-size: 16px;">Weekly report</h3>
  <p style="margin: 0; font-size: 14px;">Generated 2 hours ago.</p>
</md-card>
<md-card variant="outlined" style="inline-size: 200px;">
  <h3 style="margin: 0; font-size: 16px;">Weekly report</h3>
  <p style="margin: 0; font-size: 14px;">Generated 2 hours ago.</p>
</md-card>

Everything is default-slotted content. There is no headline, media or actions slot — the card is a container, not a template, and the structure is yours. The host lays its children out in a column flexbox and reads --md-card-padding and --md-card-gap for the spacing — it does not define them, so they compute to nothing until you set one; the built-in fallbacks are 16px and 8px at density 0 (and taper from there, see Density). Plain children stack correctly without a wrapper.

Composed content — heading, body, divider, actions
Reports

Weekly report

Generated 2 hours ago from 1,204 events.

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

<md-card variant="outlined" style="inline-size: 280px;">
  <span style="font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--md-sys-color-on-surface-variant);">Reports</span>
  <h3 style="margin: 0; font-size: 16px;">Weekly report</h3>
  <p style="margin: 0; font-size: 14px;">Generated 2 hours ago from 1,204 events.</p>
  <md-divider></md-divider>
  <div style="display: flex; gap: 8px;">
    <md-button variant="text">Open</md-button>
    <md-button variant="text">Share</md-button>
  </div>
</md-card>

Because the structure is yours, the same container scales up to a fully composed card — media bleeding to the edges, an avatar row, body copy and an action row. Note the negative margins on the image: they cancel the card’s own padding so the media reaches the corners, which is the one piece of plumbing a slot-based card would have done for you.

A fully composed card Open in Storybook
Ada Lovelace
Posted 2 hours ago

A card is a container, not a template — media, a header row, body copy and actions are all just slotted children stacked by the card's own column flexbox.

Cancel Share
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-card variant="elevated" style="max-inline-size: 400px;">
  <img src="https://picsum.photos/seed/complex/400/180" alt="" style="inline-size: calc(100% + 32px); max-inline-size: none; block-size: 180px; object-fit: cover; display: block; margin: -16px -16px 0;">
  <div style="display: flex; align-items: center; gap: 12px;">
    <div style="inline-size: 40px; block-size: 40px; border-radius: 50%; background: var(--md-sys-color-primary-container); display: flex; align-items: center; justify-content: center;">
      <span class="material-symbols-outlined" aria-hidden="true" style="font-size: 20px; color: var(--md-sys-color-on-primary-container);">person</span>
    </div>
    <div>
      <div style="font-weight: 500; font-size: 16px;">Ada Lovelace</div>
      <div style="font-size: 12px; color: var(--md-sys-color-on-surface-variant);">Posted 2 hours ago</div>
    </div>
  </div>
  <p style="margin: 0; font-size: 14px; line-height: 1.5; color: var(--md-sys-color-on-surface-variant);">
    A card is a container, not a template — media, a header row, body copy and actions are all just slotted children stacked by the card's own column flexbox.
  </p>
  <div style="display: flex; gap: 8px; justify-content: flex-end;">
    <md-button variant="outlined">Cancel</md-button>
    <md-button variant="filled">Share</md-button>
  </div>
</md-card>

interactive makes the whole card the control: the host takes role="button", tabindex="0", Enter/Space activation, a state layer and a ripple, and emits mdClick.

Interactive cards — click, or tab and press Enter Open in Storybook

Weekly report

The whole card is the control.

Monthly report

Tab to it, then press Enter.

Annual report

Ripple and state layer included.

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

<md-card variant="elevated" interactive aria-label="Open weekly report" style="inline-size: 220px;">
  <h3 style="margin: 0; font-size: 16px;">Weekly report</h3>
  <p style="margin: 0; font-size: 14px;">The whole card is the control.</p>
</md-card>
<md-card variant="filled" interactive aria-label="Open monthly report" style="inline-size: 220px;">
  <h3 style="margin: 0; font-size: 16px;">Monthly report</h3>
  <p style="margin: 0; font-size: 14px;">Tab to it, then press Enter.</p>
</md-card>
<md-card variant="outlined" interactive aria-label="Open annual report" style="inline-size: 220px;">
  <h3 style="margin: 0; font-size: 16px;">Annual report</h3>
  <p style="margin: 0; font-size: 14px;">Ripple and state layer included.</p>
</md-card>

An interactive card also needs a real accessible name. Its inner text is not automatically a good one — give it aria-label.

The ripple is on by default. ripple="false" drops the ripple animation and keeps everything else — the hover, focus and press state layers stay, because they are a separate element.

The host sets overflow: hidden, so an image or video clips to the corner radius without extra work. Cancel the card’s own padding on a full-bleed image with negative margins, and remember M3’s contrast rule: text over a busy image needs a scrim or a bounding shape.

Image and video media Open in Storybook

Mountain pass

Text sits below the image, not on it.

Clip preview

A video behaves exactly like an image.

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

<md-card variant="elevated" style="inline-size: 300px;">
  <img src="https://picsum.photos/seed/md3card/400/200" alt="" style="inline-size: calc(100% + 32px); max-inline-size: none; block-size: 160px; object-fit: cover; display: block; margin: -16px -16px 0;">
  <h3 style="margin: 0; font-size: 16px;">Mountain pass</h3>
  <p style="margin: 0; font-size: 14px; color: var(--md-sys-color-on-surface-variant);">Text sits below the image, not on it.</p>
</md-card>
<md-card variant="outlined" style="inline-size: 300px;">
  <video muted loop autoplay playsinline preload="auto" aria-label="Sample clip" style="inline-size: calc(100% + 32px); max-inline-size: none; aspect-ratio: 16 / 9; object-fit: cover; display: block; background: #000; margin: -16px -16px 0;" src="https://www.w3schools.com/html/mov_bbb.mp4"></video>
  <h3 style="margin: 0; font-size: 16px;">Clip preview</h3>
  <p style="margin: 0; font-size: 14px; color: var(--md-sys-color-on-surface-variant);">A video behaves exactly like an image.</p>
</md-card>

The second card above is a <video> on exactly the same terms as the image — negative margins to reach the padding edge, and the card clipping it to the container shape.

StateFocusableEffect
disabledNoInert and out of the tab order; aria-disabled when the card acts as a button
soft-disabledYesSame visuals, still focusable so the card stays discoverable

Both only mean anything on an interactive (or drag-enabled) card — a plain container has nothing to disable.

States — try tabbing through them Open in Storybook

Enabled

Clickable and focusable.

Disabled

Out of the tab order.

Soft-disabled

Still reachable by keyboard.

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

<md-card variant="filled" interactive aria-label="Available report" style="inline-size: 200px;">
  <h3 style="margin: 0; font-size: 16px;">Enabled</h3>
  <p style="margin: 0; font-size: 14px;">Clickable and focusable.</p>
</md-card>
<md-card variant="filled" interactive disabled aria-label="Unavailable report" style="inline-size: 200px;">
  <h3 style="margin: 0; font-size: 16px;">Disabled</h3>
  <p style="margin: 0; font-size: 14px;">Out of the tab order.</p>
</md-card>
<md-card variant="filled" interactive soft-disabled aria-label="Locked report" style="inline-size: 200px;">
  <h3 style="margin: 0; font-size: 16px;">Soft-disabled</h3>
  <p style="margin: 0; font-size: 14px;">Still reachable by keyboard.</p>
</md-card>

full-width and full-height make the card fill its container, which is what you want inside a CSS grid so cards in a row stay even when translated text changes their length. For anything more specific there are --md-card-width / -min-width / -max-width and the matching --md-card-height properties.

A grid of equal-height cards Open in Storybook

Short

One line.

Longer

Several more words of supporting text that wrap onto extra lines.

Medium

A middling amount of text.

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: repeat(auto-fill, minmax(180px, 1fr)); gap: 16px; inline-size: 100%;">
  <md-card variant="outlined" full-width full-height>
    <h3 style="margin: 0; font-size: 16px;">Short</h3>
    <p style="margin: 0; font-size: 14px;">One line.</p>
  </md-card>
  <md-card variant="outlined" full-width full-height>
    <h3 style="margin: 0; font-size: 16px;">Longer</h3>
    <p style="margin: 0; font-size: 14px;">Several more words of supporting text that wrap onto extra lines.</p>
  </md-card>
  <md-card variant="outlined" full-width full-height>
    <h3 style="margin: 0; font-size: 16px;">Medium</h3>
    <p style="margin: 0; font-size: 14px;">A middling amount of text.</p>
  </md-card>
</div>

For a card that should not follow its container, set the size properties directly:

Fixed width, fixed box, and a cap Open in Storybook

Fixed width

220px regardless of the container.

Fixed box

220 × 160, content clipped to fit.

Capped

Grows with its content up to 260px, then wraps instead of stretching.

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: flex-start; flex-wrap: wrap;">
  <md-card variant="outlined" style="--md-card-width: 220px;">
    <h3 style="margin: 0; font-size: 16px;">Fixed width</h3>
    <p style="margin: 0; font-size: 14px;">220px regardless of the container.</p>
  </md-card>
  <md-card variant="outlined" style="--md-card-width: 220px; --md-card-height: 160px;">
    <h3 style="margin: 0; font-size: 16px;">Fixed box</h3>
    <p style="margin: 0; font-size: 14px;">220 × 160, content clipped to fit.</p>
  </md-card>
  <md-card variant="outlined" style="--md-card-max-width: 260px;">
    <h3 style="margin: 0; font-size: 16px;">Capped</h3>
    <p style="margin: 0; font-size: 14px;">Grows with its content up to 260px, then wraps instead of stretching.</p>
  </md-card>
</div>

Cards are fluid by default, so a card in a responsive grid needs no breakpoint markup — see the responsiveness story.

drag-enabled reports the gesture. It emits mdDragStart, mdDragMove and mdDragEnd, mirrors the state in aria-grabbed, and — once the gesture actually starts, not merely on pointer-down — applies the dragged state layer and pins a raised elevation for its duration. It does translate the card under the pointer and lift it above its siblings for the duration — but that transform is undone the instant you release, and nothing is reordered. The card tells you what the pointer did; the list is yours.

The gesture only begins once the pointer has travelled 5px or more from where it went down, so an ordinary click on a drag-enabled card is still a click — it never picks up the dragged visuals — and a real drag suppresses the click that would otherwise follow it.

Drag-enabled tiles — the card follows the pointer, then returns Open in Storybook

Tile A

Press and drag.

Tile B

Let go — it snaps back.

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

<md-card variant="elevated" drag-enabled style="inline-size: 180px;">
  <h3 style="margin: 0; font-size: 16px;">Tile A</h3>
  <p style="margin: 0; font-size: 14px;">Press and drag.</p>
</md-card>
<md-card variant="elevated" drag-enabled style="inline-size: 180px;">
  <h3 style="margin: 0; font-size: 16px;">Tile B</h3>
  <p style="margin: 0; font-size: 14px;">Let go — it snaps back.</p>
</md-card>

The component deliberately stops at reporting, so a working reorder is these events plus a few lines of yours. This demo does exactly that: on mdDragEnd it looks at where the pointer landed and moves the tile in the DOM.

A working reorder — the component supplies the gesture, you supply the order Open in Storybook

Tile A

Drag me sideways.

Tile B

Drop me on a neighbour.

Tile C

The order is real DOM order.

Drag a tile onto one of its neighbours and let go.
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<div id="strip" style="display: flex; gap: 12px;">
  <md-card variant="elevated" drag-enabled>Tile A</md-card>
  <md-card variant="elevated" drag-enabled>Tile B</md-card>
  <md-card variant="elevated" drag-enabled>Tile C</md-card>
</div>

<script type="module">
  var strip = document.getElementById('card-reorder-strip');
  var log = document.getElementById('card-reorder-log');

  root.addEventListener('mdDragEnd', function (e) {
    var tile = e.target;
    var tiles = Array.prototype.slice.call(strip.children);
    var from = tiles.indexOf(tile);
    var to = from;

    for (var i = 0; i < tiles.length; i++) {
      if (tiles[i] === tile) continue;
      var r = tiles[i].getBoundingClientRect();
      if (e.detail.clientX >= r.left && e.detail.clientX <= r.right) to = i;
    }

    if (to === from) {
      log.textContent = 'Dropped clear of the other tiles — order unchanged.';
      return;
    }

    strip.insertBefore(tile, to > from ? tiles[to].nextSibling : tiles[to]);
    log.textContent = 'Tile ' + tile.dataset.label + ' moved from slot ' + (from + 1) + ' to slot ' + (to + 1) + '.';
  });
</script>

The same reorder in each stack. In the framework versions the order is state and the cards are rendered from it, so the drop handler only has to move an array entry — detail.offsetX divided by the tile pitch is how many slots the pointer travelled:

<div id="strip" style="display: flex; gap: 12px;">
<md-card variant="elevated" drag-enabled>Tile A</md-card>
<md-card variant="elevated" drag-enabled>Tile B</md-card>
<md-card variant="elevated" drag-enabled>Tile C</md-card>
</div>

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

strip.addEventListener('mdDragEnd', (e) => {
  const tiles = [...strip.children];
  const from = tiles.indexOf(e.target);
  const to = tiles.findIndex((t) => {
    if (t === e.target) return false;
    const r = t.getBoundingClientRect();
    return e.detail.clientX >= r.left && e.detail.clientX <= r.right;
  });

  if (to === -1 || to === from) return;
  strip.insertBefore(e.target, to > from ? tiles[to].nextSibling : tiles[to]);
});
</script>
EventCancelableDetailFires
mdClicknoMouseEventCard activated (pointer, Enter, Space) — interactive only
mdDragStartnoMdCardDragDetailPointer moved past the 5px drag threshold
mdDragMovenoMdCardDragDetailEvery pointer move during the drag
mdDragEndnoMdCardDragDetailPointer released
interface MdCardDragDetail {
clientX: number; // pointer position, viewport coordinates
clientY: number;
offsetX: number; // distance from the drag start point
offsetY: number;
startX: number; // where the gesture began
startY: number;
}

mdClick carries the underlying MouseEvent as its detail, so e.detail.type === 'click' even when the activation came from the keyboard path.

Both the drag trio and mdClick bubble and are composed, so one delegated listener on a wrapper catches every card in a collection. Drag the right-hand tile and click the left-hand card:

One delegated listener catches both cards

Click me

Or tab here and press Enter.

Drag me

Past 5px, then the trio fires.

One listener on the wrapper is logging both cards.
Show code for each technology
<md-card id="report" variant="elevated" interactive aria-label="Open weekly report">
<h3>Weekly report</h3>
<p>Generated 2 hours ago.</p>
</md-card>

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

card.addEventListener('mdClick', (e) => {
  console.log(e.detail.type);          // 'click' — the underlying MouseEvent
  location.assign('/reports/weekly');
});
</script>

Properties

PropertyAttributeTypeDefaultReflects
variantvariant'elevated' | 'filled' | 'outlined''elevated'
interactiveinteractivebooleanfalseYes
dragEnableddrag-enabledbooleanfalseYes
disableddisabledbooleanfalseYes
softDisabledsoft-disabledbooleanfalseYes
rippleripplebooleantrue
fullWidthfull-widthbooleanfalseYes
fullHeightfull-heightbooleanfalseYes
densitydensity0 | -1 | -2 | -3 | -40Yes

Slots

SlotDescription
(default)

CSS Custom Properties

Override on the host element for per-instance theming:

PropertyDescription
--md-card-container-colorContainer background
--md-card-container-shapeBorder-radius override
--md-card-container-elevationBox-shadow override
--md-card-outline-colorOutline color (outlined variant)
--md-card-outline-widthOutline width (outlined variant)
--md-card-state-layer-colorState layer color override
--md-card-widthExplicit inline-size (default: auto)
--md-card-min-widthMinimum inline-size
--md-card-max-widthMaximum inline-size
--md-card-heightExplicit block-size (default: auto)
--md-card-min-heightMinimum block-size
--md-card-max-heightMaximum block-size
--md-card-padding
--md-card-gap

CSS Shadow Parts

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

PartDescription
state-layerHover/press overlay (interactive only)
outlineBorder element (outlined variant only)
  • Decide the interaction model once. Either the card is a control (interactive, with an aria-label and keyboard activation) or it is a container holding its own controls. Both at once is the classic nested-interactive failure — and the component resolves it by dropping the button semantics, not by warning you.
  • An interactive card needs a real accessible name via aria-label. Its inner text isn’t automatically a good one.
  • Headings inside the card should fit the page’s heading hierarchy — a card is not a reason to restart at h1.
  • Text over images needs verified contrast; use a bounding shape or a scrim.
  • disabled leaves the tab order; soft-disabled stays focusable, so an unavailable-but-worth-discovering card is still reachable.
  • Drag is pointer-only. If reordering matters, provide a keyboard path of your own.

Tab through these four. The first is a single tab stop that announces as a button. The second is marked interactive too, but its own buttons are the tab stops — the card detected them and quietly kept a plain container role rather than nesting focusables inside a button. The third is skipped entirely; the fourth still takes focus and announces its disabled visuals.

Keyboard and screen-reader behaviour Open in Storybook

Card is the control

role=button, one tab stop, Enter activates.

Card holds controls

The buttons are the tab stops.

Open Share

disabled

Out of the tab order entirely.

soft-disabled

Still focusable, so still discoverable.

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

<md-card variant="outlined" interactive aria-label="Open the weekly report" style="inline-size: 220px;">
  <h3 style="margin: 0; font-size: 16px;">Card is the control</h3>
  <p style="margin: 0; font-size: 14px;">role=button, one tab stop, Enter activates.</p>
</md-card>
<md-card variant="outlined" interactive style="inline-size: 220px;">
  <h3 style="margin: 0; font-size: 16px;">Card holds controls</h3>
  <p style="margin: 0; font-size: 14px;">The buttons are the tab stops.</p>
  <div style="display: flex; gap: 8px;">
    <md-button variant="text">Open</md-button>
    <md-button variant="text">Share</md-button>
  </div>
</md-card>
<md-card variant="outlined" interactive disabled aria-label="Report unavailable" style="inline-size: 220px;">
  <h3 style="margin: 0; font-size: 16px;">disabled</h3>
  <p style="margin: 0; font-size: 14px;">Out of the tab order entirely.</p>
</md-card>
<md-card variant="outlined" interactive soft-disabled aria-label="Report locked" style="inline-size: 220px;">
  <h3 style="margin: 0; font-size: 16px;">soft-disabled</h3>
  <p style="margin: 0; font-size: 14px;">Still focusable, so still discoverable.</p>
</md-card>

The second card is the one worth dwelling on: nothing warns you. interactive is still set, the card still ripples and still emits mdClick on a pointer press — only the keyboard affordance you thought you added is absent, because role="button" and tabindex were never applied. If the whole card must be keyboard-activatable, take the inner controls out. The accessibility story walks the same roles and keyboard model side by side.

RTL — the card’s own metrics are logical. Padding, gap, the column flow and the inline-size bounds all mirror under dir="rtl" with nothing re-authored. What does not mirror for free is the layout CSS you write for your slotted content — the card has no named slots, so there is no template doing it for you. See RTL.

<div dir="rtl">
<md-card variant="outlined"></md-card>
</div>

The same composed card in both directions. Only dir changes between the rows:

Same markup, dir=ltr vs dir=rtl Open in Storybook
ltr

Weekly report

Padding, gap and flow are logical, so the card mirrors on its own.

Cancel Save

Clickable

Same behaviour in either direction.

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:start;">
  <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;flex-wrap:wrap;">
    <md-card variant="outlined" style="inline-size: 260px;">
      <h3 style="margin: 0; font-size: 16px;">Weekly report</h3>
      <p style="margin: 0; font-size: 14px;">Padding, gap and flow are logical, so the card mirrors on its own.</p>
      <md-divider></md-divider>
      <div style="display: flex; gap: 8px; justify-content: flex-end;">
        <md-button variant="text">Cancel</md-button>
        <md-button variant="filled">Save</md-button>
      </div>
    </md-card>
    <md-card variant="elevated" interactive aria-label="Open weekly report" style="inline-size: 200px;">
      <h3 style="margin: 0; font-size: 16px;">Clickable</h3>
      <p style="margin: 0; font-size: 14px;">Same behaviour in either direction.</p>
    </md-card>
  </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;flex-wrap:wrap;">
    <md-card variant="outlined" style="inline-size: 260px;">
      <h3 style="margin: 0; font-size: 16px;">التقرير الأسبوعي</h3>
      <p style="margin: 0; font-size: 14px;">الحشو والتدفق منطقيان، لذا تنعكس البطاقة تلقائيًا.</p>
      <md-divider></md-divider>
      <div style="display: flex; gap: 8px; justify-content: flex-end;">
        <md-button variant="text">إلغاء</md-button>
        <md-button variant="filled">حفظ</md-button>
      </div>
    </md-card>
    <md-card variant="elevated" interactive aria-label="فتح التقرير الأسبوعي" style="inline-size: 200px;">
      <h3 style="margin: 0; font-size: 16px;">قابلة للنقر</h3>
      <p style="margin: 0; font-size: 14px;">نفس السلوك في الاتجاهين.</p>
    </md-card>
  </div>
</div>

Physical offsets in your content don’t mirror — logical ones do

Section titled “Physical offsets in your content don’t mirror — logical ones do”

The full-bleed media trick from Media is symmetric (margin: -16px -16px 0), so it is direction-agnostic and needs no attention. Alignment does: justify-content: right pins an action row to the physical right, which under RTL is the leading edge — the wrong side. flex-end follows the direction instead. Both rows below are dir="rtl", and only the action row’s CSS differs:

Action-row alignment — flex-end vs right, both under dir=rtl
correct

حفظ التغييرات

justify-content: flex-end — الأزرار على الحافة اللاحقة.

إلغاء حفظ
wrong

حفظ التغييرات

justify-content: right — الأزرار على الحافة الأمامية.

إلغاء حفظ
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:start;">
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">correct</span>
  <div dir="rtl" style="display:flex;gap:16px;flex-wrap:wrap;">
    <md-card variant="outlined" style="inline-size: 280px;">
      <h3 style="margin: 0; font-size: 16px;">حفظ التغييرات</h3>
      <p style="margin: 0; font-size: 14px;">justify-content: flex-end — الأزرار على الحافة اللاحقة.</p>
      <div style="display: flex; gap: 8px; justify-content: flex-end;">
        <md-button variant="text">إلغاء</md-button>
        <md-button variant="filled">حفظ</md-button>
      </div>
    </md-card>
  </div>
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">wrong</span>
  <div dir="rtl" style="display:flex;gap:16px;flex-wrap:wrap;">
    <md-card variant="outlined" style="inline-size: 280px;">
      <h3 style="margin: 0; font-size: 16px;">حفظ التغييرات</h3>
      <p style="margin: 0; font-size: 14px;">justify-content: right — الأزرار على الحافة الأمامية.</p>
      <div style="display: flex; gap: 8px; justify-content: right;">
        <md-button variant="text">إلغاء</md-button>
        <md-button variant="filled">حفظ</md-button>
      </div>
    </md-card>
  </div>
</div>

density is a local override of the inherited data-density, and the spacing values all derive from that one signal, so the box tapers together: padding 16 → 14 → 12 → 10 → 8px, gap 8 → 7 → 6 → 5 → 4px, and the body typescale with them. Padding and gap floor at 8px and 4px rather than collapsing. The corner radius does not taper — it resolves to --md-sys-shape-corner-medium, a flat 12px from the token layer; the density-scaled value in the CSS is only the fallback for pages that load the component without those tokens. The card’s height does not step evenly either — tighter padding leaves more room for text, so a card can lose a wrapped line and jump:

Density 0 through -4 — padding, gap and type taper; the 12px radius holds
0

Weekly report

Generated 2 hours ago.

Weekly report

Generated 2 hours ago.

-1

Weekly report

Generated 2 hours ago.

Weekly report

Generated 2 hours ago.

-2

Weekly report

Generated 2 hours ago.

Weekly report

Generated 2 hours ago.

-3

Weekly report

Generated 2 hours ago.

Weekly report

Generated 2 hours ago.

-4

Weekly report

Generated 2 hours ago.

Weekly report

Generated 2 hours ago.

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>
  <div style="display:flex;gap:12px;flex-wrap:wrap;align-items:flex-start;"><md-card variant="outlined" style="inline-size: 200px;"><h3 style="margin: 0; font-size: 15px;">Weekly report</h3><p style="margin: 0;">Generated 2 hours ago.</p></md-card><md-card variant="filled" style="inline-size: 200px;"><h3 style="margin: 0; font-size: 15px;">Weekly report</h3><p style="margin: 0;">Generated 2 hours ago.</p></md-card></div>
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">-1</span>
  <div style="display:flex;gap:12px;flex-wrap:wrap;align-items:flex-start;"><md-card variant="outlined" density="-1" style="inline-size: 200px;"><h3 style="margin: 0; font-size: 15px;">Weekly report</h3><p style="margin: 0;">Generated 2 hours ago.</p></md-card><md-card variant="filled" density="-1" style="inline-size: 200px;"><h3 style="margin: 0; font-size: 15px;">Weekly report</h3><p style="margin: 0;">Generated 2 hours ago.</p></md-card></div>
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">-2</span>
  <div style="display:flex;gap:12px;flex-wrap:wrap;align-items:flex-start;"><md-card variant="outlined" density="-2" style="inline-size: 200px;"><h3 style="margin: 0; font-size: 15px;">Weekly report</h3><p style="margin: 0;">Generated 2 hours ago.</p></md-card><md-card variant="filled" density="-2" style="inline-size: 200px;"><h3 style="margin: 0; font-size: 15px;">Weekly report</h3><p style="margin: 0;">Generated 2 hours ago.</p></md-card></div>
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">-3</span>
  <div style="display:flex;gap:12px;flex-wrap:wrap;align-items:flex-start;"><md-card variant="outlined" density="-3" style="inline-size: 200px;"><h3 style="margin: 0; font-size: 15px;">Weekly report</h3><p style="margin: 0;">Generated 2 hours ago.</p></md-card><md-card variant="filled" density="-3" style="inline-size: 200px;"><h3 style="margin: 0; font-size: 15px;">Weekly report</h3><p style="margin: 0;">Generated 2 hours ago.</p></md-card></div>
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">-4</span>
  <div style="display:flex;gap:12px;flex-wrap:wrap;align-items:flex-start;"><md-card variant="outlined" density="-4" style="inline-size: 200px;"><h3 style="margin: 0; font-size: 15px;">Weekly report</h3><p style="margin: 0;">Generated 2 hours ago.</p></md-card><md-card variant="filled" density="-4" style="inline-size: 200px;"><h3 style="margin: 0; font-size: 15px;">Weekly report</h3><p style="margin: 0;">Generated 2 hours ago.</p></md-card></div>
</div>

They are independent signals, so they compose with no extra wiring — and a card can set its own density, which wins over the rung it inherited:

dir=rtl with data-density=-2, and one card overriding to -4

كثافة موروثة

حشو أضيق من الحاوية.

إلغاء حفظ

قابلة للنقر

نفس الرونغ، مع طبقة الحالة.

رونغ محلي ‎-4

الرونغ المحلي يتجاوز الموروث.

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;flex-wrap:wrap;align-items:flex-start;">
  <md-card variant="outlined" style="inline-size: 220px;">
    <h3 style="margin: 0; font-size: 16px;">كثافة موروثة</h3>
    <p style="margin: 0;">حشو أضيق من الحاوية.</p>
    <div style="display: flex; gap: 8px; justify-content: flex-end;">
      <md-button variant="text">إلغاء</md-button>
      <md-button variant="filled">حفظ</md-button>
    </div>
  </md-card>
  <md-card variant="elevated" interactive aria-label="بطاقة قابلة للنقر" style="inline-size: 220px;">
    <h3 style="margin: 0; font-size: 16px;">قابلة للنقر</h3>
    <p style="margin: 0;">نفس الرونغ، مع طبقة الحالة.</p>
  </md-card>
  <md-card variant="outlined" density="-4" style="inline-size: 220px;">
    <h3 style="margin: 0; font-size: 16px;">رونغ محلي ‎-4</h3>
    <p style="margin: 0;">الرونغ المحلي يتجاوز الموروث.</p>
  </md-card>
</div>

Densitydensity="-1…-4" locally overrides the inherited data-density rung (0 is the default, not an override). See Density.

i18n — all text is your slotted content, so translation is entirely yours (the localization story shows a translated set). Translated text changes card height; in a grid, prefer full-height so cards in a row stay even.

Custom propertyPurposeDefault
--md-card-container-colorSurface fillPer variant
--md-card-container-shapeCorner radius12px
--md-card-container-elevationBox-shadow override — a full box-shadow value, not a level; ignored while draggingPer variant
--md-card-outline-color / -widthoutlined borderoutline-variant / 1px
--md-card-state-layer-colorHover/press overlay (interactive)on-surface
--md-card-width / -min-width / -max-widthInline-size boundsauto / auto / none
--md-card-height / -min-height / -max-heightBlock-size boundsauto / auto / none
--md-card-padding / --md-card-gapInternal spacing16px / 8px
Themed instances Open in Storybook

Squared off

4px radius, 24px padding.

Primary outline

2px primary border.

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

<md-card variant="filled" style="inline-size: 220px; --md-card-container-shape: 4px; --md-card-padding: 24px;">
  <h3 style="margin: 0; font-size: 16px;">Squared off</h3>
  <p style="margin: 0; font-size: 14px;">4px radius, 24px padding.</p>
</md-card>
<md-card variant="outlined" style="inline-size: 220px; --md-card-outline-color: var(--md-sys-color-primary); --md-card-outline-width: 2px;">
  <h3 style="margin: 0; font-size: 16px;">Primary outline</h3>
  <p style="margin: 0; font-size: 14px;">2px primary border.</p>
</md-card>

Check any override in both themes — the elevated variant leans on shadow, which reads very differently on a dark surface. See the dark-theme story.

CSS partsstate-layer, outline. Use them for what the colour tokens cannot express: a gradient state layer, a dashed or thicker outline.

::part() overrides Open in Storybook

Dashed outline

::part(outline) restyled.

Gradient state layer

Hover me — ::part(state-layer).

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

<style>
  .card-parts-a::part(outline) { border-style: dashed; border-width: 2px; border-color: var(--md-sys-color-primary); }
  .card-parts-b::part(state-layer) { background: linear-gradient(135deg, var(--md-sys-color-primary), var(--md-sys-color-tertiary)); opacity: .18; }
</style>
<div style="display: flex; gap: 16px; flex-wrap: wrap;">
  <md-card variant="outlined" class="card-parts-a" style="inline-size: 220px;">
    <h3 style="margin: 0; font-size: 16px;">Dashed outline</h3>
    <p style="margin: 0; font-size: 14px;">::part(outline) restyled.</p>
  </md-card>
  <md-card variant="filled" interactive class="card-parts-b" style="inline-size: 220px;">
    <h3 style="margin: 0; font-size: 16px;">Gradient state layer</h3>
    <p style="margin: 0; font-size: 14px;">Hover me — ::part(state-layer).</p>
  </md-card>
</div>
md-card::part(outline) {
border-style: dashed;
}

md-list · md-list-item · md-dialog · md-accordion · md-divider · md-button · md-ripple

For AI Agents — md-card

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

# md-card

<!-- llm:meta
tag: md-card
category: containment
status: md3-mapped
m3-guidelines: https://m3.material.io/components/cards/guidelines
form-associated: false
depends-on: md-ripple
used-by: none
-->

**A container for related content and actions about a single subject.** Three
variants (elevated, filled, outlined), an optional interactive mode with ripple
and state layers, and an optional pointer-drag gesture that reports movement but
never reorders anything itself.

> 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

- Grouping **content and actions about one subject** — a product, an article, a
  contact — especially when several appear together as a collection.
- An entry point into a detail view (`interactive`).
- Reorderable tiles, where you own the reorder logic (`drag-enabled`).

## When NOT to use

| Situation | Use instead |
|---|---|
| A vertical list of records | `md-list` + `md-list-item` |
| A blocking decision | `md-dialog` |
| A transient message | `md-snackbar` |
| Content that spacing or a heading would organise better | Nothing — M3 warns against forcing content into cards |
| Collapsible sections | `md-accordion` |
| A single action | `md-button` |

## Decision cues

| Need | Setting |
|---|---|
| Default, subtly raised | `variant="elevated"` (default) |
| Flat, tonal surface | `variant="filled"` |
| Defined edge, no shadow | `variant="outlined"` |
| Whole card is one control | `interactive` (+ `mdClick`) |
| Interactive, but no ripple | `interactive ripple="false"` |
| Reorderable | `drag-enabled` (+ `mdDragStart` / `mdDragMove` / `mdDragEnd`) |
| Fill its grid cell | `full-width` / `full-height` |
| Cap a fluid card | `full-width` + `--md-card-max-width` |

## API contract

```html
<md-card
  variant="elevated|filled|outlined"   <!-- default: elevated -->
  interactive                          <!-- default: false -->
  drag-enabled                         <!-- default: false -->
  disabled                             <!-- default: false -->
  soft-disabled                        <!-- default: false -->
  ripple="true|false"                  <!-- default: true -->
  full-width                           <!-- default: false -->
  full-height                          <!-- default: false -->
  density="-1|-2|-3|-4"                <!-- default: 0 (uncompacted; only -1…-4 have rules) -->
>
  <h3>Headline</h3>
  <p>Supporting text</p>
</md-card>
```

**Events** — `mdClick` (`CustomEvent<MouseEvent>`), `mdDragStart` /
`mdDragMove` / `mdDragEnd` (`CustomEvent<MdCardDragDetail>`). All four are
default Stencil events, so they bubble and cross shadow boundaries.
`MdCardDragDetail` is `{ clientX, clientY, offsetX, offsetY, startX, startY }`,
where `offsetX`/`offsetY` are the distance travelled from the drag start point.

**Methods** — none.

**Slots** — one unnamed default slot. There are no named slots: the card is a
container, not a template, so media, headline and actions are your markup. The
host is `display: flex; flex-direction: column`, so slotted children stack and
are separated by `--md-card-gap`.

**Parts** — `state-layer` (rendered only when `interactive` or `drag-enabled`),
`outline` (rendered only for `variant="outlined"`).

### Behavioral contract worth knowing

- `mdClick` fires **only when `interactive` is set**. Without it the click
  handler returns immediately, so a plain card is inert.
- **The card decides its own `role`.** When `interactive` and the slotted
  content holds no focusable controls, the host sets `role="button"` and
  `tabindex="0"`, and Enter/Space activate it. Do not set `role` yourself —
  the render pass writes the attribute and will overwrite yours.
- **Focusable content demotes the card.** If the slotted content contains a
  focusable control (native `a[href]`, `button`, `input`, `select`,
  `textarea`, `[contenteditable]`, widget ARIA roles, or a library tag such as
  `md-button`, `md-icon-button`, `md-checkbox`, `md-switch`, `md-text-field`,
  `md-select`, `md-slider`, `md-chip`…), the host drops `role="button"` and
  `tabindex` to avoid a `nested-interactive` violation. It still emits
  `mdClick` on mouse click, but there is then **no keyboard path to the card
  action** — so either the card is the control, or its children are.
  The scan runs before first render and again on `slotchange` and on any
  light-DOM mutation, so it stays correct for content added later.
- `disabled` removes the card from the tab order (`tabindex="-1"`) and sets
  `pointer-events: none`.
- **`soft-disabled` is focusable but inert — not clickable.** It keeps
  `tabindex="0"` and still receives pointer events, but both the click handler
  and the keydown handler bail on `disabled || soft-disabled`, so a
  `soft-disabled` card emits **no `mdClick`** and does not activate on
  Enter / Space. Use it when the card must stay reachable and announceable
  (so a screen-reader user can find out why it is unavailable), not when you
  still want the action to run. Both flags also block the drag gesture.
- **The two flags mute the card differently.** The `disabled` look is gated on
  `interactive`: the host only gets its disabled class when
  `interactive && (disabled || soft-disabled)`, so `<md-card disabled>` or
  `<md-card drag-enabled disabled>` keeps its normal colours (the gesture is
  still blocked). The `soft-disabled` look is ungated — it comes from a plain
  `:host([soft-disabled])` rule — so even a bare `<md-card soft-disabled>`
  renders muted.
- **`drag-enabled` reports the gesture; it does not reorder anything.** The
  drag starts only after the pointer moves 5px (Manhattan distance) with the
  primary button held. While dragging, the component writes `transform` and
  `z-index: 1000` to the host's inline style and clears both on release, so the
  card snaps back to its original position — persisting a new order is yours.
- While dragging the card raises its own elevation (`elevation-4` elevated,
  `elevation-3` filled/outlined). You do not need to set elevation by hand.
- A click generated at the end of a drag is suppressed for one animation
  frame, so a drag never fires `mdClick`.
- `drag-enabled` also mirrors `aria-grabbed` (`"false"` at rest, `"true"` while
  dragging) onto the host.
- The host is `overflow: hidden` — a card never scrolls internally.

---

## Do / Don't

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

| ✅ Do | ❌ Don't |
|---|---|
| Show cards together as a collection | Don't force content into cards when spacing, headlines, or dividers would give a simpler hierarchy |
| Expand a card to reveal more information | Don't scroll **within** a card to reveal information |
| On mobile, let a card expand and the page scroll | Don't nest a scroller in a card — two scrollbars is the failure mode |
| Assign at most **one** swipe action to a card | Don't put swipeable content (carousels, pagination) inside a card, and don't let parts detach on swipe |
| Let elevation rise while a card is being moved | Don't let a dragged card bump other elements aside — it floats above everything except app bars and navigation |
| Ensure text over images meets contrast standards | Don't place text directly on a busy image without a bounding shape |
| Keep one subject per card | Don't mix unrelated content in one card |

---

## Patterns

```html
<!-- Content card with its own actions — NOT interactive -->
<md-card variant="elevated">
  <img src="/cover.jpg" alt="" style="inline-size: 100%; border-radius: 12px;">
  <h3>Weekly report</h3>
  <p>Generated 2 hours ago.</p>
  <md-divider></md-divider>
  <md-button variant="text">Open</md-button>
  <md-button variant="text">Share</md-button>
</md-card>
```

```html
<!-- Interactive card: the WHOLE card is the control, so no inner controls -->
<md-card id="report-card" interactive variant="outlined"
         aria-label="Open weekly report">
  <h3>Weekly report</h3>
  <p>Generated 2 hours ago.</p>
</md-card>

<script type="module">
  document
    .getElementById('report-card')
    .addEventListener('mdClick', () => {
      window.location.href = '/reports/weekly';
    });
</script>
```

```html
<!-- Draggable tile — the card reports the gesture, you do the reordering -->
<md-card id="tile" drag-enabled>Tile A</md-card>

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

  tile.addEventListener('mdDragMove', (e) => {
    // e.detail: { clientX, clientY, offsetX, offsetY, startX, startY }
    highlightDropZoneAt(e.detail.clientX, e.detail.clientY);
  });

  tile.addEventListener('mdDragEnd', (e) => {
    // The card has already snapped back — commit the new order yourself.
    commitReorder(e.detail.offsetX, e.detail.offsetY);
  });

  function highlightDropZoneAt() {}
  function commitReorder() {}
</script>
```

```html
<!-- Grid of equal-height cards, each capped at 360px -->
<div style="display: grid;
            grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
            gap: 16px;">
  <md-card full-width full-height style="--md-card-max-width: 360px;">
    <h3>One</h3>
  </md-card>
  <md-card full-width full-height style="--md-card-max-width: 360px;">
    <h3>Two</h3>
  </md-card>
</div>
```

## Anti-patterns

| ❌ Wrong | ✅ Right | Why |
|---|---|---|
| `interactive` card that also holds `md-button`s | Pick one: clickable card **or** card with actions | The host detects the focusable children and drops `role="button"`/`tabindex`, so the card action becomes mouse-only. |
| `<md-card interactive role="link">` | Let the card set its own `role` | Render writes `role="button"` on the host and overwrites the attribute. |
| Listening for `mdClick` on a card without `interactive` | Add `interactive` | The click handler returns early when `interactive` is false. |
| Setting `--md-card-container-elevation` on drag start | Nothing — the dragged state raises elevation itself | The `dragged` state rule sets the elevation directly and wins over the custom property. |
| `--md-card-container-elevation: 3` | `--md-card-container-elevation: var(--md-sys-elevation-3)` | The property is a `box-shadow` value, not an elevation number. |
| Expecting `drag-enabled` to reorder or to leave the card where it was dropped | Handle `mdDragEnd` and commit the order yourself | The component clears the inline `transform` on release. |
| `overflow: auto` on a card | Let the page scroll, or expand the card | The host is `overflow: hidden`; M3 says cards don't scroll internally. |
| Looking for `headline` / `media` / `actions` slots | Compose freely in the default slot | The card has no named slots. |
| `<md-card headline="…">` | Slot an `<h3>` | There is no `headline` prop. |
| A card per row of a plain list | `md-list` | Cards are heavy for simple rows. |
| `disabled` on a card that is not `interactive` | `interactive` + `disabled`, or `soft-disabled` when you only want the muted look | The disabled *styling* is gated on `interactive`; `drag-enabled` alone does not produce it (the drag gesture is still blocked either way). |
| Listening for `mdClick` on a `soft-disabled` card | Drop `soft-disabled`, or use plain `disabled` if it really is unavailable | The click and keydown handlers both return early on `soft-disabled`, so the card is focusable but emits nothing. |
| Text straight onto a photo | Add a scrim or bounding shape | M3 contrast caution. |
| Wrapping the card in an `<a href>` | Use `interactive` + `mdClick` | The focusable-content scan only looks at the card's own descendants, so an ancestor anchor never demotes it — you ship `role="button"` nested inside a link, a real nested-interactive violation. |

## Accessibility, RTL, density, i18n

**Accessibility**
- **Decide the interaction model once**: either the card is the control
  (`interactive`, with an `aria-label`, keyboard-activatable) or it is a
  container holding its own controls. The component enforces this by demoting
  its role when it finds focusable content, so trying to do both silently
  costs you the keyboard path.
- An `interactive` card needs a real accessible name — give it `aria-label`
  (or `aria-labelledby` pointing at an id in the same light-DOM tree). Its
  slotted text is not automatically a good name.
- Headings inside the card should fit the page's heading hierarchy.
- Text over images needs verified contrast; use a bounding shape or scrim.
- On a card that acts as a button (`interactive`, with no focusable slotted
  content), `disabled` sets `tabindex="-1"` and `aria-disabled="true"`, and
  `soft-disabled` sets `aria-disabled="true"` while keeping `tabindex="0"` —
  so screen-reader users can still reach it and are told it is unavailable.
  Neither attribute is written on a card that is not acting as a button.
- An `interactive` card shows a 3px `--md-sys-color-secondary` focus ring on
  `:focus-visible`.
- Drag is pointer-only — there is no keyboard drag. Always provide another way
  to reorder.

**RTL** — padding, sizing and content flow use logical properties
(`inline-size`, `block-size`, `padding`), so the card mirrors automatically.

**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 card out of an inherited
rung; use `style="--md-sys-density-scale: 0"` to reset the scale locally.
Density drives the card's padding, gap, corner radius, font-size and
line-height.

**i18n** — all text is your slotted content. Translated text changes card
height; in a grid, prefer `full-height` so cards in a row stay even.

## Related components

`md-list` · `md-list-item` · `md-dialog` · `md-accordion` · `md-divider` ·
`md-button` · `md-ripple`

## Theming

| Custom property | Purpose | Default |
|---|---|---|
| `--md-card-container-color` | Container background | `--md-sys-color-surface-container-low` (elevated), `--md-sys-color-surface-container-highest` (filled), `--md-sys-color-surface` (outlined) |
| `--md-card-container-shape` | Corner radius | `--md-sys-shape-corner-medium`, falling back to `max(8px, 12px + density)` |
| `--md-card-container-elevation` | Resting `box-shadow` | `--md-sys-elevation-1` (elevated), `--md-sys-elevation-0` (filled, outlined) |
| `--md-card-outline-color` | `outlined` border colour | `--md-sys-color-outline-variant` |
| `--md-card-outline-width` | `outlined` border width | `1px` |
| `--md-card-state-layer-color` | Hover/press overlay colour | `--md-sys-color-on-surface` |
| `--md-card-width` | Explicit inline-size | `auto` (`100%` with `full-width`) |
| `--md-card-min-width` | Minimum inline-size | `auto` |
| `--md-card-max-width` | Maximum inline-size | `none` |
| `--md-card-height` | Explicit block-size | `auto` (`100%` with `full-height`) |
| `--md-card-min-height` | Minimum block-size | `auto` |
| `--md-card-max-height` | Maximum block-size | `none` |
| `--md-card-padding` | Internal padding | `max(8px, 16px + density × 2px)` |
| `--md-card-gap` | Gap between slotted children | `max(4px, 8px + density × 1px)` |

Hover, focus, pressed and dragged states set elevation directly and therefore
override `--md-card-container-elevation`; it controls the resting shadow.

**CSS parts** — `state-layer` (interactive/draggable only) and `outline`
(outlined only).

```css
md-card.dashboard-tile::part(outline) {
  border-style: dashed;
}

md-card.dashboard-tile {
  --md-card-container-color: var(--md-sys-color-surface-container);
  --md-card-max-width: 420px;
}
```

<!-- Auto Generated Below -->


## Properties

| Property       | Attribute       | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | Type                                   | Default      |
| -------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | ------------ |
| `density`      | `density`       | Local density rung. Drives the same `--md-sys-density-scale` signal that a global `data-density` ancestor sets, so a local value simply overrides the inherited one. 0 = default, -4 = ultra-compact.                                                                                                                                                                                                                                                                                                                                                                     | `-1 \| -2 \| -3 \| -4 \| 0`            | `0`          |
| `disabled`     | `disabled`      | Disables the card — only meaningful when interactive                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `boolean`                              | `false`      |
| `dragEnabled`  | `drag-enabled`  | Enables pointer-event-based drag on the card                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | `boolean`                              | `false`      |
| `fullHeight`   | `full-height`   | When `true` the card stretches to 100% of its container's block-size. The parent must have a definite block-size (e.g. a fixed height row in a CSS grid, or a flex item with `align-items: stretch`) for `100%` to resolve.                                                                                                                                                                                                                                                                                                                                               | `boolean`                              | `false`      |
| `fullWidth`    | `full-width`    | When `true` the card stretches to 100% of its container's inline-size. Pair with the `--md-card-max-width` custom property to cap the fluid width (useful inside dashboards and responsive grids).                                                                                                                                                                                                                                                                                                                                                                        | `boolean`                              | `false`      |
| `interactive`  | `interactive`   | Makes the card interactive (clickable, with ripple and state layers).  When the card's own content has no focusable controls the host exposes `role="button"` + `tabindex` and is keyboard-activatable. If the content *does* include its own focusable controls (buttons, links, form fields, interactive `md-*` components) the host does not claim `role="button"` — that would be a `nested-interactive` a11y violation — and instead stays a plain container that is still mouse-clickable (emits `mdClick`) while the inner controls remain the keyboard tab stops. | `boolean`                              | `false`      |
| `ripple`       | `ripple`        | Whether the ripple effect is enabled when interactive                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | `boolean`                              | `true`       |
| `softDisabled` | `soft-disabled` | Soft-disabled: disabled visuals but remains focusable — only meaningful when interactive                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | `boolean`                              | `false`      |
| `variant`      | `variant`       | Visual style variant                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `"elevated" \| "filled" \| "outlined"` | `'elevated'` |


## Events

| Event         | Description                                                                                   | Type                            |
| ------------- | --------------------------------------------------------------------------------------------- | ------------------------------- |
| `mdClick`     | Emits when an interactive card is clicked or activated via keyboard                           | `CustomEvent<MouseEvent>`       |
| `mdDragEnd`   | Emits when a draggable card is released after dragging                                        | `CustomEvent<MdCardDragDetail>` |
| `mdDragMove`  | Emits continuously as a dragged card moves — use for drop-zone detection or position tracking | `CustomEvent<MdCardDragDetail>` |
| `mdDragStart` | Emits once when a draggable card starts being dragged (after crossing the movement threshold) | `CustomEvent<MdCardDragDetail>` |


## Shadow Parts

| Part            | Description |
| --------------- | ----------- |
| `"outline"`     |             |
| `"state-layer"` |             |


## Dependencies

### Depends on

- [md-ripple](../md-ripple)

### Graph
```mermaid
graph TD;
  md-card --> md-ripple
  style md-card 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.