Skip to content

Button

A discrete, committed action — Save, Confirm, Next. Five emphasis levels, five sizes, round or square with M3-Expressive shape morphing, optional toggle mode, and native form participation via ElementInternals.

Live preview Open in Storybook
Filled Tonal Elevated Outlined Text
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-button variant="filled">Filled</md-button>
<md-button variant="tonal">Tonal</md-button>
<md-button variant="elevated">Elevated</md-button>
<md-button variant="outlined">Outlined</md-button>
<md-button variant="text">Text</md-button>

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


<md-button>Save</md-button>
  • A discrete action the user commits to: Save, Confirm, Join now.
  • Actions inside dialogs, forms, cards, toolbars and bottom sheets.
  • A binary toggle with a visible label — Save / Favorite / Follow.
  • A submit or reset control inside a <form>.
SituationUse instead
Icon-only affordance, no labelmd-icon-button
Navigating between app destinationsmd-navigation-bar, md-navigation-rail, or a text link
The single most prominent action (mobile)md-fab
3+ related actions acting as one unitmd-button-group
One primary action plus variations of itmd-split-button
Filtering, attributes, removable entriesmd-chip
Low-priority actions crowding the UIOverflow md-menu

Pick by emphasis, highest first. M3: use the strongest style sparingly — ideally one filled button per screen.

VariantEmphasisUse for
filledHighestThe one flow-completing action
tonalHigh-ishLower priority, more weight than an outline
elevatedMediumNeeds separation from a busy background
outlinedMediumImportant, but not primary
textLowestSeveral options side by side; dialogs, cards
All five variants
Filled Tonal Elevated Outlined Text
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-button variant="filled">Filled</md-button>
<md-button variant="tonal">Tonal</md-button>
<md-button variant="elevated">Elevated</md-button>
<md-button variant="outlined">Outlined</md-button>
<md-button variant="text">Text</md-button>
SizeHeightTypical use
xs32pxDense tables, toolbars, inline row actions
sm40pxDefault. Standard UI density
md48pxComfortable / touch-first forms
lg56pxPrimary CTA on a landing surface
xl96pxHero / marketing CTA
Sizes
Extra small Small Medium Large
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-button size="xs">Extra small</md-button>
<md-button size="sm">Small</md-button>
<md-button size="md">Medium</md-button>
<md-button size="lg">Large</md-button>

Every variant is available at every size. The full matrix, so you can see how weight and scale interact:

Every variant at every size
filled XS SM MD LG XL
tonal XS SM MD LG XL
elevated XS SM MD LG XL
outlined XS SM MD LG XL
text XS SM MD LG XL
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:12px;align-items:center;margin-block-end:12px;">
  <span style="inline-size:4.5rem;opacity:.7;font-size:.8rem;">filled</span>
  <md-button variant="filled" size="xs">XS</md-button>
  <md-button variant="filled" size="sm">SM</md-button>
  <md-button variant="filled" size="md">MD</md-button>
  <md-button variant="filled" size="lg">LG</md-button>
  <md-button variant="filled" size="xl">XL</md-button>
</div>
<div style="display:flex;gap:12px;align-items:center;margin-block-end:12px;">
  <span style="inline-size:4.5rem;opacity:.7;font-size:.8rem;">tonal</span>
  <md-button variant="tonal" size="xs">XS</md-button>
  <md-button variant="tonal" size="sm">SM</md-button>
  <md-button variant="tonal" size="md">MD</md-button>
  <md-button variant="tonal" size="lg">LG</md-button>
  <md-button variant="tonal" size="xl">XL</md-button>
</div>
<div style="display:flex;gap:12px;align-items:center;margin-block-end:12px;">
  <span style="inline-size:4.5rem;opacity:.7;font-size:.8rem;">elevated</span>
  <md-button variant="elevated" size="xs">XS</md-button>
  <md-button variant="elevated" size="sm">SM</md-button>
  <md-button variant="elevated" size="md">MD</md-button>
  <md-button variant="elevated" size="lg">LG</md-button>
  <md-button variant="elevated" size="xl">XL</md-button>
</div>
<div style="display:flex;gap:12px;align-items:center;margin-block-end:12px;">
  <span style="inline-size:4.5rem;opacity:.7;font-size:.8rem;">outlined</span>
  <md-button variant="outlined" size="xs">XS</md-button>
  <md-button variant="outlined" size="sm">SM</md-button>
  <md-button variant="outlined" size="md">MD</md-button>
  <md-button variant="outlined" size="lg">LG</md-button>
  <md-button variant="outlined" size="xl">XL</md-button>
</div>
<div style="display:flex;gap:12px;align-items:center;margin-block-end:12px;">
  <span style="inline-size:4.5rem;opacity:.7;font-size:.8rem;">text</span>
  <md-button variant="text" size="xs">XS</md-button>
  <md-button variant="text" size="sm">SM</md-button>
  <md-button variant="text" size="md">MD</md-button>
  <md-button variant="text" size="lg">LG</md-button>
  <md-button variant="text" size="xl">XL</md-button>
</div>

shape sets the resting geometry. shape-morph (default true) animates the radius on press and on toggle — pressed buttons square off by one step; round toggles morph to square when selected, and square toggles morph to round.

Shapes — press and hold each to see the morph
Round Square Morph disabled
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-button variant="filled" shape="round">Round</md-button>
<md-button variant="filled" shape="square">Square</md-button>
<md-button variant="tonal" shape="round" shape-morph="false">Morph disabled</md-button>

Disable morphing app-wide with data-shape-morph="off" on <html>. See Shape Morph.

A leading icon is the M3 default. Icons come from the icon / trailing-icon props, or the matching named slots for custom glyphs (SVG, icon font, emoji) — a slot replaces the corresponding prop.

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

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

<md-button variant="filled" icon="add">Add item</md-button>
<md-button variant="outlined" trailing-icon="arrow_forward">Next</md-button>
<md-button variant="tonal">
  <svg slot="leading-icon" viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
  Starred
</md-button>

disabled, soft-disabled and loading all make the button inert — loading is not merely cosmetic.

StateFocusableUse when
disabledNo (tabindex="-1")The action is unavailable and undiscoverable
soft-disabledYesContextually unavailable but worth discovering — M3’s preference
loadingNoAn async operation is in flight
States — try tabbing through them
Enabled Disabled Soft-disabled Saving…
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-button variant="filled">Enabled</md-button>
<md-button variant="filled" disabled>Disabled</md-button>
<md-button variant="filled" soft-disabled>Soft-disabled</md-button>
<md-button variant="filled" loading>Saving…</md-button>

loading swaps the leading icon for a spinner — an md-loading-indicator, not a bespoke ring — and it inherits the label colour, so it reads correctly on every variant:

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

<md-button variant="filled" loading>filled</md-button>
<md-button variant="outlined" loading>outlined</md-button>
<md-button variant="text" loading>text</md-button>
<md-button variant="elevated" loading>elevated</md-button>
<md-button variant="tonal" loading>tonal</md-button>

Slot your own into loader when the default isn’t the right affordance — a circular md-progress-indicator, a branded mark, anything. The slot replaces the whole spinner; everything else about the loading state (inert, aria-busy, the label staying put) is unchanged.

The built-in md-loading-indicator, and an md-progress-indicator slotted over it
Default Swapped
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-button variant="filled" loading>Default</md-button>

<md-button variant="filled" loading>
  Swapped
  <md-progress-indicator slot="loader" variant="circular" indeterminate size="18" label="Saving"></md-progress-indicator>
</md-button>

The spinner tracks the size tier and the density rung, because it is derived from the icon box rather than a fixed pixel value:

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

<md-button variant="filled" size="xs" loading>XS</md-button>
<md-button variant="filled" size="sm" loading>SM</md-button>
<md-button variant="filled" size="md" loading>MD</md-button>
<md-button variant="filled" size="lg" loading>LG</md-button>
<md-button variant="filled" size="xl" loading>XL</md-button>

loading is the built-in path. When you need the progress API instead — determinate values, a wavy indicator, four-colour — slot an md-progress-indicator into leading-icon or trailing-icon. Leading reads as the spinner being the subject of the action, trailing as its outcome:

Slotted md-progress-indicator — leading and trailing Open in Storybook
leading
Saving Syncing 65% done
trailing
Continue Uploading Exporting
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:4.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">leading</span>
  <div style="display:flex;gap:12px;flex-wrap:wrap;align-items:center;">
    <md-button variant="filled" size="md" soft-disabled>
      <md-progress-indicator slot="leading-icon" variant="circular" indeterminate label="Saving"></md-progress-indicator>
      Saving
    </md-button>
    <md-button variant="outlined" size="md" soft-disabled>
      <md-progress-indicator slot="leading-icon" variant="circular" indeterminate label="Syncing"></md-progress-indicator>
      Syncing
    </md-button>
    <md-button variant="tonal" size="md" soft-disabled>
      <md-progress-indicator slot="leading-icon" variant="circular" value="65" label="65 percent"></md-progress-indicator>
      65% done
    </md-button>
  </div>
  <span style="inline-size:4.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">trailing</span>
  <div style="display:flex;gap:12px;flex-wrap:wrap;align-items:center;">
    <md-button variant="filled" size="md" soft-disabled>
      Continue
      <md-progress-indicator slot="trailing-icon" variant="circular" indeterminate label="Working"></md-progress-indicator>
    </md-button>
    <md-button variant="outlined" size="md" soft-disabled>
      Uploading
      <md-progress-indicator slot="trailing-icon" variant="circular" indeterminate label="Uploading"></md-progress-indicator>
    </md-button>
    <md-button variant="tonal" size="md" soft-disabled>
      Exporting
      <md-progress-indicator slot="trailing-icon" variant="circular" indeterminate wave four-color label="Exporting"></md-progress-indicator>
    </md-button>
  </div>
</div>

toggle turns the button into a two-state control and emits aria-pressed automatically.

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

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

<md-button variant="tonal" toggle icon="favorite" value="fav">Follow</md-button>
<md-button variant="filled" toggle selected value="sub">Subscribed</md-button>

Keep the label length stable between states — M3 warns that a label swinging from “Follow” to “You are now following” disrupts the layout.

Content-sized by default. full-width stretches to the container; cap it so it never becomes a long flat bar.

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

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

<md-button variant="filled" full-width icon="check">Confirm and continue</md-button>
<md-button variant="outlined" full-width style="--md-button-max-width: 360px;">Capped at 360px</md-button>

For arbitrary sizing use --md-button-width, --md-button-min-width, --md-button-max-width, --md-button-height, --md-button-min-height.

Explicit width and height
Fixed 260px Min 200px Tall 64px 180 x 56
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-button variant="filled" style="--md-button-width: 260px;">Fixed 260px</md-button>
<md-button variant="outlined" style="--md-button-min-width: 200px;">Min 200px</md-button>
<md-button variant="tonal" style="--md-button-height: 64px;">Tall 64px</md-button>
<md-button variant="filled" style="--md-button-width: 180px; --md-button-height: 56px;">180 x 56</md-button>

Set href and the button navigates instead of acting.

<md-button variant="text" href="/settings">Settings</md-button>
<md-button variant="text" href="https://example.com" target="_blank">External</md-button>
Link buttons — these navigate
M3 guidelines GitHub Browse components
Show code for each technology
<!-- index.html <head> — the icon font the components draw from -->
<link rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">

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

<md-button variant="filled" href="https://m3.material.io" target="_blank" trailing-icon="open_in_new">M3 guidelines</md-button>
<md-button variant="outlined" href="https://github.com" target="_blank" trailing-icon="open_in_new">GitHub</md-button>
<md-button variant="text" href="/components/" icon="link">Browse components</md-button>

For SPA routing, cancel mdClick and route yourself:

<md-button variant="text" href="/settings" id="nav">Settings</md-button>

<script type="module">
document.getElementById('nav').addEventListener('mdClick', (e) => {
  const { href } = e.detail;
  if (href?.startsWith('/')) {
    e.preventDefault();      // cancels navigation AND the toggle flip
    router.push(href);
  }
});
</script>

md-button is form-associated via ElementInternals, so type="submit" and type="reset" work across the shadow boundary — no hidden <input> needed. Submit calls form.requestSubmit(), so the form’s submit event fires and native constraint validation runs.

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

<form id="profile">
  <md-text-field label="Email" type="email" name="email" required></md-text-field>

  <md-button variant="filled" type="submit">Save</md-button>
  <md-button variant="text" type="reset">Reset</md-button>
</form>
<form id="profile">
<md-text-field label="Email" type="email" name="email" required></md-text-field>

<md-button variant="filled" type="submit">Save</md-button>
<md-button variant="text" type="reset">Reset</md-button>
</form>

<script type="module">
document.getElementById('profile').addEventListener('submit', (e) => {
  e.preventDefault();
  const data = new FormData(e.target);
  console.log(Object.fromEntries(data));   // { email: '…' }
});
</script>
EventCancelableDetailFires
mdClickyesMdButtonClickDetailEvery activation (pointer, Enter, Space)
mdChangeno{ selected, value }Only when toggle mode flips selected

mdChange’s payload is a strict subset of mdClick’s, so the difference is not the data — it is the guarantee.

mdClick.detail.selected is a prediction: the state the button will take if nothing cancels the click. A listener registered after yours can still call preventDefault(), so a handler that commits on mdClick can commit a change that never happened. mdChange fires after the flip and cannot be vetoed, so it is the committed fact.

This is the same split the platform uses for <input type="checkbox">click is cancelable and predictive, change is settled — including the part people forget: neither fires on a programmatic change. Setting btn.selected = true in code emits nothing, exactly as input.checked = true emits nothing. If your own code changes the state, your own code already knows.

You want to…Listen to
Veto the activation (guard, confirm, permission check)mdClick + preventDefault()
React to a committed togglemdChange
Handle a plain, non-toggle button pressmdClick
Know the previous statemdClickdetail.selected is the incoming one
interface MdButtonClickDetail {
value: string;
toggle: boolean;
selected: boolean; // the state the button WILL take — a before-change hook
href: string;
target: string;
originalEvent: MouseEvent | KeyboardEvent;
}

Both events bubble and are composed, so one delegated listener on an ancestor catches every button.

One delegated listener catches both buttons
Favorite Vetoed on click
Toggle either button. The second one cancels its own click.
Show code for each technology
<md-button id="save" variant="filled" toggle value="fav">Favorite</md-button>

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

btn.addEventListener('mdClick', (e) => {
  if (!confirm('Continue?')) e.preventDefault();   // veto
});

btn.addEventListener('mdChange', (e) => {
  console.log(e.detail.value, e.detail.selected);
});
</script>

Properties

PropertyAttributeTypeDefaultReflects
typetype'button' | 'submit' | 'reset''button'Yes
variantvariant'filled' | 'outlined' | 'text' | 'elevated' | 'tonal''filled'
sizesize'xs' | 'sm' | 'md' | 'lg' | 'xl''sm'
shapeshape'round' | 'square''round'
disableddisabledbooleanfalseYes
softDisabledsoft-disabledbooleanfalseYes
rippleripplebooleantrue
loadingloadingbooleanfalse
iconiconstring''
trailingIcontrailing-iconstring''
hrefhrefstring''
targettargetstring'_self'
toggletogglebooleanfalse
selectedselectedbooleanfalse
shapeMorphshape-morphbooleantrue
mirrorIconmirror-iconbooleanfalseYes
suppressExpandIconFlipsuppress-expand-icon-flipbooleanfalseYes
fullWidthfull-widthbooleanfalseYes
connectedLeftconnected-leftbooleanfalseYes
connectedRightconnected-rightbooleanfalseYes
valuevaluestring''
groupTabindexgroup-tabindexnumber | nullnull
roleOverriderole-overridestring''
densitydensity0 | -1 | -2 | -3 | -40Yes

Slots

SlotDescription
(default)
leading-iconCustom leading icon (replaces icon prop)
trailing-iconCustom trailing icon (replaces trailingIcon prop)
loader

CSS Custom Properties

Override on the host element for per-instance theming:

PropertyDescription
--md-button-container-colorContainer background
--md-button-label-colorLabel + icon text color
--md-button-icon-colorIcon-only color override
--md-button-icon-sizeIcon dimensions
--md-button-container-shapeBorder-radius override
--md-button-outline-colorOutlined border color
--md-button-outline-widthOutlined border width
--md-button-widthFixed inline-size (default: auto)
--md-button-min-widthMinimum inline-size (default: per-size touch target)
--md-button-max-widthMaximum inline-size (default: none)
--md-button-heightFixed block-size (default: auto)
--md-button-min-heightMinimum block-size (default: per-size touch target)
--md-button-loading-size
--md-button-focus-ring-color

CSS Shadow Parts

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

PartDescription
anchor
state-layerState-layer overlay
iconLeading icon (fallback)
labelLabel text wrapper
trailing-iconTrailing icon (fallback)
loadingLoading spinner
  • The host is the button: role="button", Enter/Space activation, and a 3px focus ring at 2px offset on every variant.
  • aria-disabled is set whenever disabled, soft-disabled or loading is active.
  • aria-pressed is emitted only in toggle mode.
  • Add aria-label whenever the visible label is insufficient. Verified against axe-core with zero WCAG 2.1 AA violations.
  • Prefer soft-disabled over disabled for contextually-unavailable actions — it stays focusable, so keyboard users can still discover it.
  • role-override exists for composite widgets (e.g. gridcell in md-date-picker). Leave it empty otherwise.

Tab through these: the soft-disabled button still takes focus, the disabled one is skipped, and the toggle announces its pressed state.

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

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

<md-button variant="filled">Focusable</md-button>
<md-button variant="filled" soft-disabled>Soft-disabled — still focusable</md-button>
<md-button variant="filled" disabled>Disabled — skipped</md-button>
<md-button variant="tonal" toggle selected value="bookmark" icon="bookmark">Saved</md-button>
<md-button variant="text" icon="delete" aria-label="Delete invoice 4021">Delete</md-button>
<md-icon-button icon="delete" aria-label="Delete invoice 4021"></md-icon-button>

The last two show the two legitimate uses of aria-label. On the md-button the visible word “Delete” is the label; aria-label overrides it with something more specific for screen-reader users, who hear “Delete invoice 4021” without the row context a sighted user gets for free. For a genuinely icon-only control, reach for md-icon-button — an md-button with an icon and no text has no accessible name and the wrong metrics, which is why it appears in Anti-patterns below.

RTL — all box metrics use logical properties, so leading/trailing icons swap automatically under dir="rtl". Add mirror-icon for directional glyphs only (arrow_forward, chevron_right, send); never for add, search or favorite. See RTL.

<div dir="rtl">
<md-button variant="filled" trailing-icon="arrow_forward" mirror-icon>التالي</md-button>
</div>

The same buttons in both directions. Nothing is re-authored between them — only dir changes. Box metrics are logical, so the icon moves to the trailing edge on its own:

Same markup, dir=ltr vs dir=rtl
ltr
Add item Next Search Remove
rtl
إضافة عنصر التالي بحث إزالة
Show code for each technology
<!-- index.html <head> — the icon font the components draw from -->
<link rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">

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

<div style="display:grid;grid-template-columns:auto 1fr;gap:14px 16px;align-items:center;">
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">ltr</span>
  <div dir="ltr" style="display:flex;gap:12px;flex-wrap:wrap;align-items:center;">
    <md-button variant="filled" icon="add">Add item</md-button>
    <md-button variant="outlined" trailing-icon="arrow_forward" mirror-icon>Next</md-button>
    <md-button variant="tonal" icon="search">Search</md-button>
    <md-button variant="text" icon="delete" trailing-icon="chevron_right" mirror-icon>Remove</md-button>
  </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:12px;flex-wrap:wrap;align-items:center;">
    <md-button variant="filled" icon="add">إضافة عنصر</md-button>
    <md-button variant="outlined" trailing-icon="arrow_forward" mirror-icon>التالي</md-button>
    <md-button variant="tonal" icon="search">بحث</md-button>
    <md-button variant="text" icon="delete" trailing-icon="chevron_right" mirror-icon>إزالة</md-button>
  </div>
</div>

Only directional glyphs flip. A magnifier, a plus or a heart points nowhere, so mirroring them just makes them look wrong. Compare the two rows under RTL:

mirror-icon — directional glyphs only
correct
التالي المزيد إرسال إضافة بحث إعجاب
wrong
التالي المزيد إرسال إضافة بحث إعجاب
Show code for each technology
<!-- index.html <head> — the icon font the components draw from -->
<link rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">

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

<div style="display:grid;grid-template-columns:auto 1fr;gap:14px 16px;align-items:center;">
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">correct</span>
  <div dir="rtl" style="display:flex;gap:12px;flex-wrap:wrap;align-items:center;">
    <md-button variant="outlined" trailing-icon="arrow_forward" mirror-icon>التالي</md-button>
    <md-button variant="outlined" trailing-icon="chevron_right" mirror-icon>المزيد</md-button>
    <md-button variant="outlined" icon="send" mirror-icon>إرسال</md-button>
    <md-button variant="tonal" icon="add">إضافة</md-button>
    <md-button variant="tonal" icon="search">بحث</md-button>
    <md-button variant="tonal" icon="favorite">إعجاب</md-button>
  </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:12px;flex-wrap:wrap;align-items:center;">
    <md-button variant="outlined" trailing-icon="arrow_forward">التالي</md-button>
    <md-button variant="outlined" trailing-icon="chevron_right">المزيد</md-button>
    <md-button variant="outlined" icon="send">إرسال</md-button>
    <md-button variant="tonal" icon="add" mirror-icon>إضافة</md-button>
    <md-button variant="tonal" icon="search" mirror-icon>بحث</md-button>
    <md-button variant="tonal" icon="favorite" mirror-icon>إعجاب</md-button>
  </div>
</div>

density sets a local rung — -1 through -4 — that wins over whatever data-density an ancestor set. The top row carries no density at all: it is the default the rungs taper away from.

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

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

<div style="display:grid;grid-template-columns:auto 1fr;gap:14px 16px;align-items:center;">
  <span style="inline-size:3.5rem;opacity:.65;font-size:.75rem;font-family:ui-monospace,monospace;">default</span>
  <div style="display:flex;gap:12px;flex-wrap:wrap;align-items:center;"><md-button variant="filled" icon="save">Save</md-button><md-button variant="outlined">Cancel</md-button><md-button variant="text" icon="delete">Delete</md-button></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:center;"><md-button variant="filled" density="-1" icon="save">Save</md-button><md-button variant="outlined" density="-1">Cancel</md-button><md-button variant="text" density="-1" icon="delete">Delete</md-button></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:center;"><md-button variant="filled" density="-2" icon="save">Save</md-button><md-button variant="outlined" density="-2">Cancel</md-button><md-button variant="text" density="-2" icon="delete">Delete</md-button></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:center;"><md-button variant="filled" density="-3" icon="save">Save</md-button><md-button variant="outlined" density="-3">Cancel</md-button><md-button variant="text" density="-3" icon="delete">Delete</md-button></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:center;"><md-button variant="filled" density="-4" icon="save">Save</md-button><md-button variant="outlined" density="-4">Cancel</md-button><md-button variant="text" density="-4" icon="delete">Delete</md-button></div>
</div>

The two are independent signals, so they compose without any extra wiring — here a dir="rtl" container at data-density="-2". The first three buttons inherit that rung; the fourth tightens further with density="-4"; the last escapes the inherited rung entirely by setting the signal itself.

dir=rtl at data-density=-2, one button at -4, one escaping back to rung 0
حفظ التالي بحث أضيق افتراضي
Show code for each technology
<!-- index.html <head> — the icon font the components draw from -->
<link rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">

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

<div dir="rtl" data-density="-2" style="display:flex;gap:12px;flex-wrap:wrap;align-items:center;">
  <md-button variant="filled" icon="save">حفظ</md-button>
  <md-button variant="outlined" trailing-icon="arrow_forward" mirror-icon>التالي</md-button>
  <md-button variant="tonal" icon="search">بحث</md-button>
  <md-button variant="tonal" density="-4" icon="search">أضيق</md-button>
  <md-button variant="text" style="--md-sys-density-scale: 0;" icon="add">افتراضي</md-button>
</div>

Densitydensity="-1…-4" locally overrides the inherited data-density rung. See Density.

i18n — the label is your slotted text, so it comes straight from your i18n layer; lang and dir are inherited from any ancestor. Localize aria-label too, and watch M3’s length-stability rule on toggle labels.

Custom propertyPurposeDefault
--md-button-container-colorContainer backgroundPer variant
--md-button-label-colorLabel + icon colorPer variant
--md-button-icon-colorIcon-only color overrideInherits label
--md-button-icon-sizeIcon box16–36px per size
--md-button-container-shapeBorder radiusPer shape/size
--md-button-outline-color / -widthoutlined borderoutline / 1px
--md-button-width / -min-width / -max-widthInline-size boundsauto / touch target / none
--md-button-height / -min-heightBlock-size boundsauto / touch target
Themed instances
Delete Squared off
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<md-button 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-button variant="filled" style="--md-button-container-shape: 4px;">Squared off</md-button>

CSS Parts reach inside the shadow root for things custom properties do not cover — part="label", part="icon", part="trailing-icon":

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

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

<style>
  .upper-btn::part(label) { text-transform: uppercase; letter-spacing: .08em; font-weight: 600; }
  .big-icon::part(icon)   { transform: scale(1.35); }
  .danger-btn {
  --md-button-container-color: #B3261E;
  --md-button-label-color: #FFFFFF;
  --md-button-container-shape: 8px;
  }
</style>
<md-button variant="filled" class="upper-btn">Uppercase label</md-button>
<md-button variant="tonal" class="big-icon" icon="favorite">Bigger icon</md-button>
<md-button variant="filled" class="danger-btn" icon="delete">Custom danger</md-button>

CSS partsstate-layer, icon, trailing-icon, label, loading.

md-button::part(label) {
letter-spacing: 0.5px;
}

md-icon-button · md-fab · md-split-button · md-button-group · md-segmented-button · md-chip · md-ripple

For AI Agents — md-button

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

# md-button

<!-- llm:meta
tag: md-button
category: actions
status: md3-mapped
m3-guidelines: https://m3.material.io/components/buttons/guidelines
form-associated: true
depends-on: md-ripple, md-loading-indicator
used-by: md-color-picker, md-date-picker, md-dialog, md-multi-select, md-snackbar, md-step, md-stepper, md-time-picker
-->

**A discrete, committed action.** Five emphasis levels, five sizes, round/square
shape with M3-Expressive shape morphing, optional toggle mode, and native form
participation (`submit`/`reset`) via `ElementInternals`.

> Setup, theming, density and i18n are configured once for the whole library —
> see [`main-llm.md`](../../../../../main-llm.md), the library-wide manual that ships
> alongside these files. Quick start:
> `import '@awc-ui/core/define';`

---

## When to use

- A **discrete action** the user commits to: `Save`, `Confirm`, `Join now`, `Next`.
- Actions inside dialogs, forms, cards, toolbars, and bottom sheets.
- A **binary toggle with a visible label** (`toggle` prop) — Save / Favorite / Follow.
- A submit or reset control inside a `<form>` (`type="submit"` / `type="reset"`).

## When NOT to use

| Situation | Use instead |
|---|---|
| Icon-only affordance (no label) | `md-icon-button` |
| Navigating between app destinations | `md-navigation-bar` / `md-navigation-rail` / `md-tabs`, or a plain text link |
| The single most prominent action on a screen (mobile-first) | `md-fab` |
| 3+ related actions that act as one unit | `md-button-group` or `md-segmented-button-set` |
| One primary action + a menu of variations | `md-split-button` |
| Filtering, attributes, or removable entries | `md-chip` |
| Low-priority actions crowding the UI | Overflow `md-menu`, or `md-icon-button` |
| Inline navigation inside a sentence | A hyperlink — **not** an underlined text button |

## Decision cues

Pick `variant` by emphasis, highest first. M3: use the strongest style **sparingly**.

| Need | `variant` | Notes |
|---|---|---|
| The one important, flow-completing action | `filled` | Ideally **only one per screen** |
| Needs separation from a busy/imagery background | `elevated` | Uses a shadow — use only when necessary |
| Lower priority, but more emphasis than an outline | `tonal` | Good for `Next` in onboarding |
| Important but not primary; pairs with `filled` | `outlined` | Place on simple backgrounds only |
| Lowest priority; several options side by side | `text` | Default in dialogs, cards, snackbars |

Pick `size` by context (**default is `sm`, not `md`**):

| `size` | Min block-size | Icon | Typical use |
|---|---|---|---|
| `xs` | 32px | 20px | Dense tables, toolbars, inline row actions |
| `sm` | 40px | 20px | **Default.** Standard UI density |
| `md` | 56px | 24px | Comfortable / touch-first forms |
| `lg` | 96px | 32px | Primary CTA on a landing surface |
| `xl` | 136px | 40px | Hero / marketing CTA |

Heights are `min-block-size` floors, not fixed heights, and each size also
carries a `min-inline-size`. The label is `white-space: nowrap`, so a long
label widens the button rather than wrapping it. All five tiers taper with
density.

## API contract

The authoritative, generated property/event tables are at the bottom of this
file. This is the short form an implementer needs.

```html
<md-button
  variant="filled|outlined|text|elevated|tonal"   <!-- default: filled -->
  size="xs|sm|md|lg|xl"                            <!-- default: sm -->
  shape="round|square"                             <!-- default: round -->
  type="button|submit|reset"                       <!-- default: button -->
  icon="add"                 trailing-icon="arrow_forward"
  href="/settings" target="_self"
  toggle selected
  disabled | soft-disabled | loading
  full-width
  mirror-icon
  density="-1|-2|-3|-4"                          <!-- omit for the default rung -->
>Label</md-button>
```

**Slots** — default (the label), `leading-icon`, `trailing-icon`, `loader` (swap the built-in spinner for your own — e.g. `md-progress-indicator variant="circular" indeterminate`) while `loading`.
A named icon slot *replaces* the matching `icon` / `trailing-icon` prop.

**Events** — both bubble and are `composed`, so one delegated listener on an
ancestor catches every button.

| Event | Cancelable | Detail | Fires |
|---|---|---|---|
| `mdClick` | **yes** | `{ value, toggle, selected, href, target, originalEvent }` | Every activation (pointer, `Enter`, `Space`) |
| `mdChange` | yes, but ignored | `{ selected, value }` | Only when `toggle` mode flips `selected` |

`detail.selected` on `mdClick` is the **post-click** state — what the button
*will* become. `preventDefault()` on `mdClick` vetoes the toggle flip **and**
the `href` navigation (the underlying DOM `click` still bubbles).

`mdChange` is dispatched with `cancelable: true` as well — Stencil's default —
so `preventDefault()` on it succeeds but changes nothing: the state has already
flipped and the component never reads `defaultPrevented`. (The generated table
below still says "Not cancelable"; that describes the intent, not the DOM
property.) `mdClick` is the only real veto hook.

**Internal props — never set these by hand:** `connectedLeft`, `connectedRight`,
`groupTabindex`. `md-button-group` owns them; setting them manually breaks the
group's roving tabindex and border-trimming.

### Behavioral contract worth knowing

- `disabled`, `soft-disabled`, **and `loading`** all make the button inert.
  `loading` is not merely cosmetic.
- `disabled` sets `tabindex="-1"` (unfocusable). `soft-disabled` keeps
  `tabindex="0"` so the action stays discoverable — this is the M3-preferred
  form for contextually-unavailable actions.
- `href` **wins over** `type`. If `href` is set, the button navigates and never
  submits or resets the form.
- Navigation goes through `window.open(sanitizeHref(href), target)`. Unsafe
  schemes (`javascript:` etc.) are refused; the form is *not* submitted as a
  fallback.
- `type="submit"` calls `form.requestSubmit()` (not `submit()`), so the form's
  `submit` event fires and constraint validation runs.

---

## Do / Don't

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

| ✅ Do | ❌ Don't |
|---|---|
| Use buttons for discrete actions | Don't clutter the UI with too many buttons — move low-priority actions into overflow menus or icon buttons |
| Let the container width follow the label text | Don't set a fixed width narrower than the label |
| Let width be responsive/stretch where it helps (`full-width`) | Don't stretch a button into a long, flat bar with very little content inside |
| Use **sentence case** — capitalize the first word and proper nouns | Don't uppercase, truncate, or wrap the label — it must stay fully visible on one line |
| Keep labels brief, ideally 1–3 words | Don't let a toggle's label change dramatically in length between states |
| Place the icon on the **leading** side, before the label | Don't use two icons in one standard button (see note below) |
| Keep the icon and label grouped and centered | Don't anchor icon and label to opposite edges of the button |
| Use a filled button for the single most important action | Don't use several filled buttons on one screen — it destroys the hierarchy |
| Use `elevated` only when the button must separate from a prominent background | Don't reach for elevation for emphasis — use `filled` instead |
| Place `outlined` and `text` buttons on simple backgrounds | Don't put `outlined`/`text` buttons over images or video without a contrasting fill |
| Use an outlined icon when a toggle is unselected, filled when selected | Don't underline a `text` button — use a real hyperlink for links |
| Keep DOM order stable across breakpoints (position may move) | Don't reorder buttons responsively — it breaks screen-reader and keyboard order |

**Two-icon note.** M3 says a standard button should carry at most one icon, yet
this component exposes both `icon` and `trailing-icon`. The trailing slot exists
for *disclosure* affordances — dropdown/expand triggers where the trailing glyph
is a chevron, not a second semantic icon. Don't pair two meaning-bearing icons.

**Outlined-vs-chip caution.** M3 warns that outlined buttons read very much like
chips. If an outlined button sits near `md-chip`s, switch it to `filled` or
`tonal`.

---

## Patterns

```html
<!-- Primary action in a form -->
<form>
  <md-text-field label="Email" required></md-text-field>
  <md-button variant="filled" type="submit">Save</md-button>
  <md-button variant="text" type="reset">Cancel</md-button>
</form>

<!-- Dialog actions: text buttons, trailing-aligned -->
<md-dialog>
  <md-button slot="actions" variant="text">Cancel</md-button>
  <md-button slot="actions" variant="filled">Confirm</md-button>
</md-dialog>

<!-- Full-width CTA, capped so it never becomes a long flat bar -->
<md-button variant="filled" full-width style="--md-button-max-width: 360px;" icon="check">
  Confirm and continue
</md-button>

<!-- Toggle with a length-stable label -->
<md-button variant="tonal" toggle value="favorite" icon="favorite">Follow</md-button>

<!-- Loading state (inert while pending) -->
<md-button variant="filled" loading>Saving…</md-button>

<!-- Contextually unavailable, still discoverable -->
<md-button variant="filled" soft-disabled>Paste</md-button>

<!-- SPA-safe link interception -->
<md-button id="settings-btn" variant="text" href="/settings">Settings</md-button>
<script>
  document.getElementById('settings-btn').addEventListener('mdClick', (e) => {
    const { href } = e.detail;
    if (href.startsWith('/')) {
      e.preventDefault();          // vetoes window.open, keeps the SPA in place
      history.pushState({}, '', href);
    }
  });
</script>

<!-- Custom iconography via slot (SVG, icon font, emoji) -->
<md-button variant="outlined">
  <svg slot="leading-icon" viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
    <path d="M12 17.3 6.2 21l1.6-6.8L2.5 9.6l6.9-.6L12 2l2.6 7 6.9.6-5.3 4.6 1.6 6.8z"/>
  </svg>
  Starred
</md-button>
```

## Anti-patterns

Mistakes that show up repeatedly in generated code.

| ❌ Wrong | ✅ Right | Why |
|---|---|---|
| `<button><md-button>Save</md-button></button>` | `<md-button>Save</md-button>` | The host *is* the button (`role="button"`, keyboard handling). Nesting produces nested interactive controls. |
| `<a href="/x"><md-button>Go</md-button></a>` | `<md-button href="/x">Go</md-button>` | Same reason; also double-activates on `Enter`. |
| `<md-button icon="delete"></md-button>` | `<md-icon-button icon="delete" aria-label="Delete">` | An icon-only `md-button` has no accessible name and the wrong metrics. |
| `<md-button size="md">` assumed to be the default | State the size explicitly | The default is **`sm`**, not `md`. |
| `md-button::part(label) { text-transform: uppercase }` | Sentence case in the label text | M3 explicitly requires sentence case. |
| Three `variant="filled"` buttons in one toolbar | One `filled`, the rest `outlined`/`text` | Competing high-emphasis actions flatten the hierarchy. |
| `disabled` on an action the user should still discover | `soft-disabled` | `disabled` removes it from tab order entirely. |
| Reading `detail.selected` as the *previous* state | It is the **next** state | It's a before-change hook by design. |
| `preventDefault()` on the native `click` to block navigation | `preventDefault()` on `mdClick` | Native `click` is not what gates the side effects. |
| Setting `connected-left` / `group-tabindex` manually | Let `md-button-group` manage them | They are `@internal`. |
| `mirror-icon` on `add` / `search` / `favorite` | Only on directional glyphs (arrows, chevrons, `send`, `reply`) | Mirroring a non-directional glyph corrupts its meaning. |
| Fixed `--md-button-width` smaller than the label | Leave width auto, or set `--md-button-max-width` | Truncated labels violate the spec. |

## Accessibility, RTL, density, i18n

**Accessibility**
- Host carries `role="button"` (override with `role-override` only for composite
  widgets, e.g. `gridcell` in `md-date-picker`), `Enter`/`Space` activation, and
  `aria-disabled` whenever `disabled`, `soft-disabled`, or `loading` is set.
- `aria-pressed` is emitted **only** in `toggle` mode.
- Put `aria-label` on the host for any button whose visible label is
  insufficient. Verified against axe-core with zero WCAG 2.1 AA violations.
- Focus ring: 3px `secondary` at 2px offset, visible on all five variants.

**RTL** — all box metrics use logical properties, so leading/trailing icons swap
automatically under `dir="rtl"`. Add `mirror-icon` for directional glyphs only.
M3 requires the icon to sit on the leading (right, in RTL) side — that is
automatic here.

**Density** — `density="-1…-4"` locally overrides the inherited
`data-density` rung; only those four rungs exist, and omitting the attribute
is the uncompacted default. `density="0"` does **not** opt a button out of an
ancestor's rung — to reset the calc-driven scale use
`style="--md-sys-density-scale: 0"` (the ancestor's `--md-sys-spacing-*`
payload still inherits). Prefer the global ancestor setting; use the prop for
one-off compact regions.

**i18n** — the label is your slotted text, so it comes straight from your i18n
layer. `lang` / `dir` are inherited from any ancestor. Localize `aria-label`
too. Watch M3's length-stability rule: a translated toggle label should not
swing wildly in length between states.

## Related components

`md-icon-button` · `md-fab` · `md-fab-menu` · `md-split-button` ·
`md-button-group` · `md-segmented-button-set` · `md-chip` · `md-ripple`

## Theming

| Custom property | Purpose | Default |
|---|---|---|
| `--md-button-container-color` | Container background | Per variant |
| `--md-button-label-color` | Label + icon color | Per variant |
| `--md-button-icon-color` | Icon-only color override | Inherits label color |
| `--md-button-icon-size` | Icon box | 20px (`xs`/`sm`) → 40px (`xl`) |
| `--md-button-container-shape` | Border radius | Per shape/size |
| `--md-button-outline-color` | `outlined` border color | `--md-sys-color-outline` |
| `--md-button-outline-width` | `outlined` border width | `1px` |
| `--md-button-width` | Fixed inline-size | `auto` (`100%` when `full-width`) |
| `--md-button-min-width` / `--md-button-max-width` | Inline-size bounds | Touch target / `none` |
| `--md-button-height` / `--md-button-min-height` | Block-size bounds | `auto` / touch target |
| `--md-button-loading-size` | Spinner box while `loading` | 70% of the icon size, 10px floor |

**CSS parts** — `state-layer`, `icon`, `trailing-icon`, `label`, `loading`.

```css
md-button.danger {
  --md-button-container-color: var(--md-sys-color-error);
  --md-button-label-color: var(--md-sys-color-on-error);
}
```

### Shape morphing

`shape-morph` (default `true`) animates the radius on press and on toggle:
pressed buttons square off by one step; `round` toggles morph to square when
selected, and `square` toggles morph to round.

<!-- 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`                  |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | `boolean`                                                   | `false`    |
| `fullWidth`              | `full-width`                | Stretch the button to fill its inline-axis container.  Switches the host from `inline-flex` to `flex` and sets `inline-size: 100%` so the button consumes the full width of its parent — the standard pattern for primary CTAs in forms, modals, and bottom sheets. The size's `min-block-size` (touch target) is preserved.  For arbitrary fixed widths or heights, set the `--md-button-width`, `--md-button-height`, `--md-button-min-width`, `--md-button-min-height`, or `--md-button-max-width` CSS custom properties on the host instead. | `boolean`                                                   | `false`    |
| `href`                   | `href`                      |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | `string`                                                    | `''`       |
| `icon`                   | `icon`                      |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | `string`                                                    | `''`       |
| `loading`                | `loading`                   |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | `boolean`                                                   | `false`    |
| `mirrorIcon`             | `mirror-icon`               | Mirror leading and trailing icons horizontally when the button is rendered in a right-to-left context. Set this on buttons that use **directional** Material Symbols (arrows, chevrons, send, reply…) — non-directional icons (add, search, favorite…) should leave it `false` so the glyph stays semantically correct.                                                                                                                                                                                                                          | `boolean`                                                   | `false`    |
| `ripple`                 | `ripple`                    |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | `boolean`                                                   | `true`     |
| `roleOverride`           | `role-override`             | Override the default `role="button"`. Used by composite widgets such as `md-date-picker` day cells (`role="gridcell"`). Leave empty for `button`.                                                                                                                                                                                                                                                                                                                                                                                                | `string`                                                    | `''`       |
| `selected`               | `selected`                  | Current selected state (toggle mode)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | `boolean`                                                   | `false`    |
| `shape`                  | `shape`                     |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | `"round" \| "square"`                                       | `'round'`  |
| `shapeMorph`             | `shape-morph`               | Enable M3 Expressive shape morphing on press and toggle                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | `boolean`                                                   | `true`     |
| `size`                   | `size`                      |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | `"lg" \| "md" \| "sm" \| "xl" \| "xs"`                      | `'sm'`     |
| `softDisabled`           | `soft-disabled`             |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | `boolean`                                                   | `false`    |
| `suppressExpandIconFlip` | `suppress-expand-icon-flip` | When true, suppresses the default 180° trailing-icon rotation while `aria-expanded="true"`. Use when the consumer swaps the glyph (e.g. docked `md-date-picker` month/year triggers use `chevron_right`).                                                                                                                                                                                                                                                                                                                                        | `boolean`                                                   | `false`    |
| `target`                 | `target`                    |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | `string`                                                    | `'_self'`  |
| `toggle`                 | `toggle`                    | Enable toggle behavior (selected/unselected)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | `boolean`                                                   | `false`    |
| `trailingIcon`           | `trailing-icon`             |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | `string`                                                    | `''`       |
| `type`                   | `type`                      | Activation behavior, mirroring the native `<button type>`: - `button` (default): no implicit form action. - `submit`: submits the associated form (`requestSubmit`, so the form's   `submit` event fires and constraint validation runs). - `reset`: resets the associated form.                                                                                                                                                                                                                                                                 | `"button" \| "reset" \| "submit"`                           | `'button'` |
| `variant`                | `variant`                   |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | `"elevated" \| "filled" \| "outlined" \| "text" \| "tonal"` | `'filled'` |


## Events

| Event      | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    | Type                                |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------- |
| `mdChange` | Fires when toggle mode flips the `selected` state in response to a user activation. Not cancelable — the state change has already happened. Pair with `mdClick` (cancelable) when you need a veto.  Bubbles and is composed.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | `CustomEvent<MdButtonChangeDetail>` |
| `mdClick`  | Fires every time the user activates the button (mouse click, touch, or `Enter`/`Space` while focused). The event is **cancelable** and the detail payload describes what *would* happen: the post-click toggle state, the navigation `href`, and so on.  Calling `event.preventDefault()` from a listener suppresses the default side effects (toggle flip + `href` navigation) but lets the underlying DOM click bubble. This is the hook to use for SPA routing, "are you sure?" prompts, async confirmation, etc.  The event bubbles and is composed, so listeners outside the shadow tree (the typical case) receive it.  ```ts document.querySelector('md-button')!.addEventListener('mdClick', (e) => {   const { href, selected } = (e as CustomEvent).detail;   if (!confirm('Continue?')) e.preventDefault(); }); ``` | `CustomEvent<MdButtonClickDetail>`  |


## Shadow Parts

| Part              | Description |
| ----------------- | ----------- |
| `"icon"`          |             |
| `"label"`         |             |
| `"loading"`       |             |
| `"state-layer"`   |             |
| `"trailing-icon"` |             |


## Dependencies

### Used by

 - [md-color-picker](../md-color-picker)
 - [md-date-picker](../md-date-picker)
 - [md-dialog](../md-dialog)
 - [md-multi-select](../md-multi-select)
 - [md-snackbar](../md-snackbar)
 - [md-step](../md-step)
 - [md-stepper](../md-stepper)
 - [md-time-picker](../md-time-picker)

### Depends on

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

### Graph
```mermaid
graph TD;
  md-button --> md-ripple
  md-button --> md-loading-indicator
  md-color-picker --> md-button
  md-date-picker --> md-button
  md-dialog --> md-button
  md-multi-select --> md-button
  md-snackbar --> md-button
  md-step --> md-button
  md-stepper --> md-button
  md-time-picker --> md-button
  style md-button 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.