Skip to content

Pie Chart

Parts of a single whole. Full pies, donuts, half-pie gauges and nested rings, with padding angles, exploded slices and a centre slot for donut KPIs. Slice colours resolve to MD3 colour roles, so dark theme and brand-token overrides re-tint automatically.

Live Preview Open in Storybook
Show code for each technology
<md-pie-chart label="Traffic" legend="right" style="--md-pie-chart-aspect-ratio: auto; --md-pie-chart-block-size: 340px;"></md-pie-chart>

<script type="module">
  const data = [
    {
      label: "Direct",
      value: 320
    },
    {
      label: "Organic search",
      value: 240
    },
    {
      label: "Paid social",
      value: 180
    },
    {
      label: "Referral",
      value: 120
    },
    {
      label: "Email",
      value: 80
    }
  ];

  const el = document.querySelector("md-pie-chart");
  Object.assign(el, { data });
</script>

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


<md-pie-chart>Pie Chart</md-pie-chart>
  • Parts of one whole, where the shares add to 100%: traffic sources, budget split, storage by type.
  • Few categories — about six at most. Past that the small slices become indistinguishable wedges.
  • A single progress or utilisation figure, as a semi-circle gauge.
SituationUse instead
Comparing magnitudes across categoriesmd-bar-chart
Change over timemd-line-chart
Composition over timemd-area-chart with stack="percentage"
Values that don’t sum to a meaningful wholemd-bar-chart
More than ~6 categoriesmd-bar-chart, sorted
Precise comparison between similar slicesmd-bar-chart — angles are hard to compare
Exact values users must readmd-table
NeedSetting
Donutinner-radius="60%"
Centre KPIthe center slot (donut only)
Half-pie gaugestart-angle="180" end-angle="0"
Separate the slicespadding-angle
Pull one slice outselected: true on the datum
One-colour rampmonochrome
Depth on the slicesgradient
Values on the slicesshow-labels (plus label-mode)
Concentric ringsring-widths
Click-throughmdSliceClick (plus drill())
Async dataloading (plus loading-label)
Reduced motionno-animation, or animation="none"
Show code for each technology
<md-pie-chart legend="bottom" style="--md-pie-chart-aspect-ratio: auto; --md-pie-chart-block-size: 300px;"></md-pie-chart>

<script type="module">
  const data = [
    {
      label: "A",
      value: 320
    },
    {
      label: "B",
      value: 240
    },
    {
      label: "C",
      value: 180
    },
    {
      label: "D",
      value: 120
    }
  ];

  const el = document.querySelector("md-pie-chart");
  Object.assign(el, { data });
</script>
Donut with centre KPI Open in Storybook
860
visitors
Show code for each technology
<md-pie-chart inner-radius="60%" legend="bottom" style="--md-pie-chart-aspect-ratio: auto; --md-pie-chart-block-size: 320px;"><div slot="center">
  <div style="font-size: 28px; font-weight: 600; line-height: 1;">860</div>
  <div style="font-size: 12px; opacity: 0.7;">visitors</div>
</div></md-pie-chart>

<script type="module">
  const data = [
    {
      label: "Direct",
      value: 320
    },
    {
      label: "Organic search",
      value: 240
    },
    {
      label: "Paid social",
      value: 180
    },
    {
      label: "Referral",
      value: 120
    }
  ];

  const el = document.querySelector("md-pie-chart");
  Object.assign(el, { data });
</script>
Storage gauge
68%
storage used
Show code for each technology
<md-pie-chart inner-radius="60%" outer-radius="95%" start-angle="180" end-angle="0" legend="none" style="--md-pie-chart-aspect-ratio: 2 / 1;"><div slot="center" style="margin-block-start: -32px;">
  <div style="font-size: 32px; font-weight: 600; line-height: 1;">68%</div>
  <div style="font-size: 12px; opacity: 0.7;">storage used</div>
</div></md-pie-chart>

<script type="module">
  const data = [
    {
      label: "Used",
      value: 68,
      color: "primary"
    },
    {
      label: "Free",
      value: 32,
      color: "surface-variant"
    }
  ];

  const el = document.querySelector("md-pie-chart");
  Object.assign(el, { data });
</script>

Set selected: true on a datum to pull it outward — useful for “this slice matters most” stories.

Exploded slice Open in Storybook
Show code for each technology
<md-pie-chart padding-angle="2" style="--md-pie-chart-aspect-ratio: auto; --md-pie-chart-block-size: 300px;"></md-pie-chart>

<script type="module">
  const data = [
    {
      label: "Direct",
      value: 320,
      selected: true
    },
    {
      label: "Organic search",
      value: 240
    },
    {
      label: "Paid social",
      value: 180
    },
    {
      label: "Referral",
      value: 120
    }
  ];

  const el = document.querySelector("md-pie-chart");
  Object.assign(el, { data });
</script>

Four numbers shape every variant. They’re independent, so a half-pie donut with separated slices is just all four set at once.

PropEffect
inner-radiusSize of the hole. 0 is a pie, 60% a donut
outer-radiusWhere the slices end, as a fraction of the available radius
start-angle / end-angleThe arc the chart sweeps. 1800 is a half-pie
padding-angleDegrees of gap between slices
corner-radiusRounds the slice ends
padding-angle separates the slices Open in Storybook
Show code for each technology
<md-pie-chart padding-angle="4" corner-radius="6" inner-radius="45%" legend="right" style="--md-pie-chart-aspect-ratio: auto; --md-pie-chart-block-size: 300px;"></md-pie-chart>

<script type="module">
  const data = [
    {
      label: "Direct",
      value: 320
    },
    {
      label: "Organic search",
      value: 240
    },
    {
      label: "Paid social",
      value: 180
    },
    {
      label: "Referral",
      value: 120
    }
  ];

  const el = document.querySelector("md-pie-chart");
  Object.assign(el, { data });
</script>

A half-pie reads as a gauge: the arc is a track, the filled part a reading. Set the aspect ratio to 2 / 1 so the box matches the shape and the chart doesn’t reserve empty space below.

A gauge built from a half donut Open in Storybook
Show code for each technology
<md-pie-chart start-angle="180" end-angle="0" inner-radius="62%" outer-radius="95%" legend="bottom" style="--md-pie-chart-aspect-ratio: 2 / 1;"></md-pie-chart>

<script type="module">
  const data = [
    {
      label: "Complete",
      value: 72,
      color: "primary"
    },
    {
      label: "Remaining",
      value: 28,
      color: "surface-variant"
    }
  ];

  const el = document.querySelector("md-pie-chart");
  Object.assign(el, { data });
</script>

A datum can carry its own radius (0–1), so the slice’s angle and its length encode two different measures — area by angle, density by reach. It is a rose / Nightingale chart, and it earns its keep only when the second measure genuinely matters; otherwise it just looks busy.

Angle = area, radius = density Open in Storybook
Show code for each technology
<md-pie-chart inner-radius="12%" outer-radius="95%" legend="bottom" style="--md-pie-chart-aspect-ratio: auto; --md-pie-chart-block-size: 460px;"></md-pie-chart>

<script type="module">
  const data = [
    {
      label: "Netherlands",
      value: 41,
      radius: 1
    },
    {
      label: "Belgium",
      value: 30,
      radius: 0.73
    },
    {
      label: "Germany",
      value: 357,
      radius: 0.53
    },
    {
      label: "France",
      value: 551,
      radius: 0.29
    },
    {
      label: "Spain",
      value: 505,
      radius: 0.22
    },
    {
      label: "Sweden",
      value: 450,
      radius: 0.06
    }
  ];

  const el = document.querySelector("md-pie-chart");
  Object.assign(el, { data });
</script>

A slice can carry children, drawn as a second ring outside it that subdivides exactly its own arc — so the inner ring answers which browser and the outer one which version of it, and the two can never disagree. Nesting the data is the API; there is no “enable rings” prop.

Families inside, versions outside Open in Storybook
Show code for each technology
<md-pie-chart label="Browser market share, January 2022" subtitle="Families inside, versions outside · StatCounter" title-align="center" inner-radius="28%" outer-radius="72%" legend="none" style="--md-pie-chart-aspect-ratio: auto; --md-pie-chart-block-size: 520px;"></md-pie-chart>

<script type="module">
  const data = [
    {
      label: "Chrome",
      value: 62.74,
      children: [
        {
          label: "v97.0",
          value: 36.89
        },
        {
          label: "v96.0",
          value: 18.16
        },
        {
          label: "v95.0",
          value: 0.54
        },
        {
          label: "Other",
          value: 7.15
        }
      ]
    },
    {
      label: "Safari",
      value: 9.86,
      children: [
        {
          label: "v15.2",
          value: 2.01
        },
        {
          label: "v15.1",
          value: 2.29
        },
        {
          label: "v14.1",
          value: 2.48
        },
        {
          label: "v13.1",
          value: 1.17
        },
        {
          label: "Other",
          value: 1.91
        }
      ]
    },
    {
      label: "Edge",
      value: 9.17,
      children: [
        {
          label: "v97",
          value: 6.62
        },
        {
          label: "v96",
          value: 2.55
        }
      ]
    },
    {
      label: "Firefox",
      value: 7.5,
      children: [
        {
          label: "v96.0",
          value: 4.17
        },
        {
          label: "v95.0",
          value: 3.33
        }
      ]
    },
    {
      label: "Other",
      value: 10.73
    }
  ];

  const valueFormatter = (v) => v.toFixed(2) + '%';

  const el = document.querySelector("md-pie-chart");
  Object.assign(el, { data, valueFormatter });
</script>

A child with no color of its own takes a lighter shade of its parent’s, so a family reads as one block with its versions as gradations. Hovering a version keeps its family lit too — they are the same answer, and dimming one would imply they were unrelated.

A parent’s value is only the label; the ring geometry comes from the children, so a child slice’s share is measured against its own level, not the whole tree — which holds every ring at once and would count each parent twice.

Internally the tree is flattened into a plain indexed list carrying level and parent, because every consumer-facing address is an index: tooltips, legend toggles, mdChartClick and the screen-reader table all say slice N. Keyboard walking follows the tree, so from a family steps into its first version rather than across to the next family.

ring-widths then tunes how the radius is divided between levels — one number per ring, read as ratios. It is a property, not an attribute:

<md-pie-chart id="rings" inner-radius="28%" outer-radius="72%"></md-pie-chart>
<script type="module">
const chart = document.querySelector('#rings');
chart.ringWidths = [3, 1]; // inner band three times the outer one
chart.data = [
{ label: 'Chrome', value: 62.74, children: [{ label: 'v97.0', value: 36.89 }] },
];
</script>

The default is deliberately not an even split: every ring but the outermost puts its labels inside its own band, so it must be wide enough to hold a word, while the outermost labels sit on leader lines and need no more than their arc.

show-labels prints on the slices; label-mode picks what: value (default), name, or both. A slice too small to hold its label doesn’t get one rather than spilling over its neighbours.

label-mode='both'
Show code for each technology
<md-pie-chart show-labels label-mode="both" legend="none" inner-radius="35%" style="--md-pie-chart-aspect-ratio: auto; --md-pie-chart-block-size: 320px;"></md-pie-chart>

<script type="module">
  const data = [
    {
      label: "Direct",
      value: 320
    },
    {
      label: "Organic",
      value: 240
    },
    {
      label: "Paid",
      value: 180
    }
  ];

  const el = document.querySelector("md-pie-chart");
  Object.assign(el, { data });
</script>

Each datum’s color takes an MD3 role name or any CSS colour. Without one, slices cycle the theme palette in order.

Explicit roles per slice Open in Storybook
Show code for each technology
<md-pie-chart inner-radius="55%" legend="right" style="--md-pie-chart-aspect-ratio: auto; --md-pie-chart-block-size: 320px;"></md-pie-chart>

<script type="module">
  const data = [
    {
      label: "Primary",
      value: 320,
      color: "primary"
    },
    {
      label: "Tertiary",
      value: 240,
      color: "tertiary"
    },
    {
      label: "Secondary",
      value: 180,
      color: "secondary"
    },
    {
      label: "Explicit hex",
      value: 120,
      color: "#e0b400"
    }
  ];

  const el = document.querySelector("md-pie-chart");
  Object.assign(el, { data });
</script>

monochrome ramps one hue from dark to light across the slices, in data order. Because the ramp follows the order, it reads as a sequence — sorted shares, stages of a funnel — and stops being arbitrary decoration.

One hue, ramped by order Open in Storybook
Show code for each technology
<md-pie-chart monochrome legend="bottom" inner-radius="45%" style="--md-pie-chart-aspect-ratio: auto; --md-pie-chart-block-size: 320px;"></md-pie-chart>

<script type="module">
  const data = [
    {
      label: "Chrome",
      value: 63
    },
    {
      label: "Safari",
      value: 20
    },
    {
      label: "Edge",
      value: 9
    },
    {
      label: "Firefox",
      value: 5
    },
    {
      label: "Other",
      value: 3
    }
  ];

  const el = document.querySelector("md-pie-chart");
  Object.assign(el, { data });
</script>

gradient shades each slice from its own colour, giving the ring depth without changing what the colours mean.

Show code for each technology
<md-pie-chart gradient inner-radius="40%" legend="right" style="--md-pie-chart-aspect-ratio: auto; --md-pie-chart-block-size: 320px;"></md-pie-chart>

<script type="module">
  const data = [
    {
      label: "Direct",
      value: 320
    },
    {
      label: "Organic search",
      value: 240
    },
    {
      label: "Paid social",
      value: 180
    },
    {
      label: "Referral",
      value: 120
    }
  ];

  const el = document.querySelector("md-pie-chart");
  Object.assign(el, { data });
</script>

legend takes an anchor — top, bottom, right, left, top-end and so on — or none. A side legend suits a pie, because the chart is square and the gutter beside it would otherwise be empty.

legend='left' Open in Storybook
Show code for each technology
<md-pie-chart legend="left" inner-radius="50%" style="--md-pie-chart-aspect-ratio: auto; --md-pie-chart-block-size: 300px;"></md-pie-chart>

<script type="module">
  const data = [
    {
      label: "Direct",
      value: 320
    },
    {
      label: "Organic search",
      value: 240
    },
    {
      label: "Paid social",
      value: 180
    }
  ];

  const el = document.querySelector("md-pie-chart");
  Object.assign(el, { data });
</script>

A donut race replays a reshuffle rather than a climb: the ring is one year’s total and the arcs are each country’s share of it. Where a line race shows values rising, this shows the composition changing — the UK holds three quarters of the total in 1965, France climbs through the 1980s, Germany’s arc closes to nothing.

The cursor is a year, not a frame index, so the slider reads as the axis it controls. Values are interpolated between yearly samples, which is what makes the arcs glide instead of stepping once a second.

Press play, or drag the year Open in Storybook
1965

drill(index) plays the transition between two levels of your own data. The chart has no idea your slices form a hierarchy: you keep the tree, decide what the next level is, and assign datadrill() only makes the swap read as a descent rather than a jump cut.

Click a slice to descend Open in Storybook
Back

Only three of the four slices lead anywhere; clicking Referral does nothing, because a slice with no children is a leaf and the handler returns early.

loading shows an opaque overlay over the plot — opaque because the engine still draws a placeholder ring when there is no data. An empty data array shows label-empty instead. The two never stack.

loading
Show code for each technology
<md-pie-chart label="Traffic" loading loading-label="Fetching traffic…" style="--md-pie-chart-aspect-ratio: auto; --md-pie-chart-block-size: 260px;"></md-pie-chart>

<script type="module">
  const data = [];

  const el = document.querySelector("md-pie-chart");
  Object.assign(el, { data });
</script>
Slotted — an indeterminate linear indicator
Fetching traffic…
Show code for each technology
<md-pie-chart label="Traffic" loading style="--md-pie-chart-aspect-ratio: auto; --md-pie-chart-block-size: 260px;"><div slot="loading" style="inline-size: min(320px, 70%); display: grid; gap: 12px; justify-items: center;">
<md-progress-indicator variant="linear" indeterminate label="Fetching traffic" style="inline-size: 100%;"></md-progress-indicator>
<span>Fetching traffic…</span>
</div></md-pie-chart>

<script type="module">
  const data = [];

  const el = document.querySelector("md-pie-chart");
  Object.assign(el, { data });
</script>
Empty state
Show code for each technology
<md-pie-chart label="Traffic" label-empty="No traffic recorded yet" style="--md-pie-chart-aspect-ratio: auto; --md-pie-chart-block-size: 260px;"></md-pie-chart>

<script type="module">
  const data = [];

  const el = document.querySelector("md-pie-chart");
  Object.assign(el, { data });
</script>

Slot your own into loader (the older name loading still works) when the default spinner isn’t the right affordance — a circular md-progress-indicator, a branded mark, a skeleton.

A circular md-progress-indicator slotted over the default loader
Show code for each technology
<md-pie-chart label="Share" loading loading-label="Fetching share…"><md-progress-indicator slot="loader" variant="circular" indeterminate size="40" label="Loading"></md-progress-indicator></md-pie-chart>

<script type="module">
  const data = [];

  const el = document.querySelector("md-pie-chart");
  Object.assign(el, { data });
</script>

tooltip selects the trigger: item reports the slice under the pointer, none disables pointer hover — keyboard navigation and the live region keep working either way. A pie has no category axis, so there is no axis reading to give: every hover is about one slice.

tooltipRenderer replaces the card’s content; the chart keeps owning the card. Return a Node, a string (set as text), { unsafeHtml }, undefined to fall back to the built-in card, or null to draw none. See the area chart’s custom tooltip for the field-by-field reference — the contract is identical across charts.

tooltipRenderer — share of the whole
Show code for each technology
<md-pie-chart label="Traffic" inner-radius="45%" legend="right" style="--md-pie-chart-aspect-ratio: auto; --md-pie-chart-block-size: 320px;"></md-pie-chart>

<script type="module">
  const data = [
    {
      label: "Direct",
      value: 320
    },
    {
      label: "Organic search",
      value: 240
    },
    {
      label: "Paid social",
      value: 180
    },
    {
      label: "Referral",
      value: 120
    }
  ];

  const tooltipRenderer = (ctx) => {
    const el = (tag, css, text) => {
      const n = document.createElement(tag);
      if (css) n.style.cssText = css;
      if (text != null) n.textContent = text;
      return n;
    };

    const row = ctx.series[0];
    if (!row) return undefined;                 // fall back to the built-in card

    const card = el('div', 'min-width:170px;font-variant-numeric:tabular-nums');
    card.append(el('div',
      'font-size:0.68em;letter-spacing:0.1em;text-transform:uppercase;opacity:0.55',
      row.label));

    const head = el('div', 'display:flex;align-items:baseline;gap:5px;margin:1px 0 6px');
    head.append(el('span', 'font-size:1.5em;font-weight:700;line-height:1.05', row.formattedValue));
    head.append(el('span', 'font-size:0.76em;opacity:0.55', 'visits'));
    card.append(head);

    const swatch = el('div', 'display:grid;grid-template-columns:9px 1fr;gap:9px;align-items:center');
    swatch.append(
      el('span', 'width:9px;height:9px;border-radius:3px;background:' + row.color),
      el('span', 'opacity:0.7', 'share of all traffic'),
    );
    card.append(swatch);
    return card;
  };

  const el = document.querySelector("md-pie-chart");
  Object.assign(el, { data, tooltipRenderer });
</script>
EventCancelableDetailFires
mdSliceClicknoMdChartClickDetail<MdPieDatum>A slice is clicked
mdLegendClickno{ dataIndex, selected }A legend chip toggles a slice
mdReadynovoidThe engine has mounted and drawn

MdChartClickDetail<MdPieDatum>mdSliceClick

FieldTypeMeaning
dataIndexnumberIndex of the slice — the value you pass to drill()
valuenumber | nullRaw value of the slice
seriesMdPieDatumThe original datum you passed in
nativeEventPointerEvent | MouseEvent | KeyboardEventA KeyboardEvent when the slice was activated from the keyboard
SlotDescription
headerRenders above the plot (toolbar, filter chips)
centerCentred in donut variants — perfect for KPI numbers
emptyEmpty-state content
footerRenders below the plot

Properties

PropertyAttributeTypeDefaultReflects
labellabelstring''
titleAligntitle-alignMdChartTitleAlign'start'
dataJS onlyMdPieDatum[][]
innerRadiusinner-radiusstring | number'0%'
outerRadiusouter-radiusstring | number'75%'
startAnglestart-anglenumber90
endAngleend-anglenumber-270
paddingAnglepadding-anglenumber0
cornerRadiuscorner-radiusnumber4
showLabelsshow-labelsbooleantrue
labelModelabel-mode'value' | 'name' | 'both'
monochromemonochromestring
gradientgradientbooleanfalse
highlighthighlight'slice' | 'series' | 'none''slice'
legendlegendMdChartLegendPosition | 'none''right'Yes
tooltiptooltipMdChartTooltipTrigger'item'
localelocalestring''
subtitlesubtitlestring | undefined
summarysummarystring''
tableLabelsJS only{ category?: string
labelEmptylabel-emptystring'No data to display'
loadingloadingbooleanfalse
loadingLabelloading-labelstring'Loading chart…'
labelPlotlabel-plotstring'Chart data. Use the arrow keys to move between slices, Home and End for the first and last, Escape to leave.'
labelPointlabel-pointstring'%label%: %value% (%percent%)'
tooltipRendererJS onlyMdChartTooltipRenderer
valueFormatterJS only(value: number) => string
ringWidthsJS onlynumber[]
heightPropheightstring
noAnimationno-animationbooleanfalse
animationanimationMdChartAnimation'expressive'
animationDurationanimation-durationnumber
densitydensity0 | -1 | -2 | -3 | -40Yes

Methods

MethodParameters
refreshTheme()none
resize()none
replay()none
drill()index: number, direction: 'down' | 'up' = 'down'
toDataURL()none
getInstance()none

Slots

SlotDescription
headerContent for the header row, replacing the default title block
centerCentred in the donut hole — a KPI number, a label, an icon
emptyReplaces the empty-state message when there is no data
loader
loadingReplaces the built-in spinner while `loading` is set
footerContent for the footer row below the plot

CSS Custom Properties

Override on the host element for per-instance theming:

PropertyDescription
--md-pie-chart-block-sizeExplicit chart height, overriding the aspect ratio (default: auto)
--md-pie-chart-min-block-sizeFloor the chart height never drops below (density-aware)
--md-pie-chart-aspect-ratioWidth:height ratio when no block-size is set (default: 1 / 1, i.e. square)
--md-pie-chart-backgroundChart surface fill (default: surface-container-low)
--md-pie-chart-paddingInset between the host edge and the plot canvas (density-aware)
--md-pie-chart-shapeCorner radius of the chart surface (density-aware)
--md-pie-chart-empty-colorEmpty-state text colour (default: on-surface-variant)
--md-pie-chart-empty-backgroundEmpty-state overlay fill (defaults to the chart background)
--md-pie-chart-empty-fontEmpty-state font family (default: body-medium)
--md-pie-chart-empty-font-sizeEmpty-state font size (default: body-medium, 14px)
--md-pie-chart-empty-icon-sizeSize of an icon slotted into the empty state
--md-pie-chart-center-colorText colour of the donut centre (the `center` slot)

CSS Shadow Parts

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

PartDescription
headerHeader row above the plot (hosts the `header` slot)
canvasThe plot surface the chart draws into
centerDonut centre overlay (hosts the `center` slot)
emptyEmpty-state overlay, shown when there is no data
loadingLoading overlay, shown while `loading` is set
footerFooter row below the plot (hosts the `footer` slot)
  • role="figure" with an accessible name that includes the slice count.
  • A screen-reader-only data table mirrors every slice — label, value and share — so the numbers are readable without seeing the shape.
  • The plot is focusable: arrow keys walk the slices, Home and End jump to the first and last, Escape leaves. label-plot announces that on focus and label-point templates each move.
  • Colour is never the only channel: the legend and the a11y table both name the slices, so a colour-blind reader is not asked to match hues.
  • RTL — the legend, the header and the tooltip mirror; the slices sweep from the same start angle either way, because a clock does not reverse in Arabic.
  • Densitydensity (0 to −4) tightens padding, the legend and the label type. It also inherits from a global data-density ancestor.
  • i18n — set locale and use Intl inside valueFormatter. Translate label, subtitle, label-empty, label-plot and label-point; label-point uses %x% / %values% tokens that must survive translation verbatim, since they are replaced by a literal string match.

In Storybook: dark theme and custom CSS.

Custom propertyPurpose
--md-pie-chart-block-size / -min-block-size / -aspect-ratioChart box
--md-pie-chart-background / -padding / -shapeSurface
--md-pie-chart-center-colorDonut centre text
--md-pie-chart-empty-color / -empty-background / -empty-font / -empty-font-size / -empty-icon-sizeEmpty state
md-pie-chart.dashboard {
--md-pie-chart-background: var(--md-sys-color-surface-container);
--md-pie-chart-padding: 16px;
--md-pie-chart-shape: 16px;
--md-pie-chart-center-color: var(--md-sys-color-primary);
}

The axis-free chart still honours the family-wide --md-chart-axis-color / --md-chart-grid-color where it draws chrome, and it reads MD3 colour roles for the slices — so a theme switch re-tints without any per-chart configuration.

CSS partsheader, canvas, center, empty, loading, footer, plus tooltip (built at runtime by the engine).

For AI Agents — md-pie-chart

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-pie-chart 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-pie-chart readme.md

# md-pie-chart

<!-- llm:meta
tag: md-pie-chart
category: charts
status: custom
m3-guidelines: none — M3 has no chart components
form-associated: false
depends-on: md-progress-indicator
used-by: none
engine: in-house Canvas2D + DOM overlay (utils/charts/engine)
-->

**Parts of a single whole.** Pie, donut (`inner-radius`), or nested rings, with
a centre slot for a headline figure and drill-down support.

> ⚠️ **Not a Material Design 3 component.** M3 ships no charts. The engine is
> **in-house Canvas2D with a DOM overlay** (`utils/charts/engine`).

> Setup, theming, density and i18n are configured once for the whole library —
> see [`main-llm.md`](../../../../../main-llm.md), the library-wide guide that ships
> alongside these component docs.

---

## When to use

- **Composition of one whole** with a small number of slices (≈ 2–6).
- A donut with a headline total in the middle (`inner-radius` + `center` slot).
- A breakdown of a breakdown, as nested rings (`children` on a slice).
- A quick "most of it is X" read.

## When NOT to use

| Situation | Use instead |
|---|---|
| More than ~6 categories | `md-bar-chart` |
| Comparing values precisely | `md-bar-chart` |
| Change over time | `md-line-chart` / `md-area-chart` |
| Composition **over time** | `md-area-chart` with `stack="percentage"` |
| Values that don't sum to a meaningful whole | `md-bar-chart` |
| Negative values | `md-bar-chart` — angles can't encode them |
| An inline micro-trend | `md-sparkline` |
| An org / node hierarchy | `md-organization-chart` |

## Decision cues

| Need | Setting |
|---|---|
| Donut | `inner-radius="60%"` |
| Headline figure in the middle | `center` slot (needs a donut) |
| Nested rings | `children` on a slice (+ `ringWidths`, a JS property) |
| Slice labels | `show-labels` (on by default) + `label-mode="value\|name\|both"` |
| Single-hue palette | `monochrome` (bare = `primary`) or `monochrome="tertiary"` |
| Separated slices | `padding-angle="2"` |
| Half-donut / gauge look | `start-angle` / `end-angle` |
| A second dimension per slice | `radius` on the datum (0–1) |
| Highlight behaviour | `highlight="slice\|series\|none"` |
| Drill into a slice | `mdSliceClick` + `await chart.drill(i)` |
| Async data | `loading` (+ `loading-label`) |

## API contract

```html
<md-pie-chart
  label="Traffic by source"
  subtitle="Last 30 days"
  title-align="start|center|end"                 <!-- default: start -->
  inner-radius="0%"                              <!-- default: 0% (a solid pie) -->
  outer-radius="75%"                             <!-- default: 75% -->
  start-angle="90"                               <!-- default: 90 (12 o'clock) -->
  end-angle="-270"                               <!-- default: -270 (a full circle) -->
  padding-angle="0"                              <!-- default: 0 -->
  corner-radius="4"                              <!-- default: 4 -->
  show-labels                                    <!-- default: ON; set show-labels="false" to hide -->
  label-mode="value|name|both"                   <!-- default: unset — resolved per ring, see below -->
  monochrome="primary"                           <!-- default: off; bare attribute = primary -->
  gradient                                       <!-- default: off -->
  highlight="slice|series|none"                  <!-- default: slice -->
  legend="top|bottom|left|right|top-start|top-end|bottom-start|bottom-end|none"
                                                 <!-- default: right -->
  tooltip="item|axis|none"                       <!-- ACCEPTED BUT INERT — see below -->
  locale="en-US"                                 <!-- default: "" (browser locale) -->
  height="320px"                                 <!-- default: aspect-ratio driven (1 / 1) -->
  summary=""                                     <!-- replaces the generated aria-label -->
  loading                                        <!-- default: off -->
  loading-label="Loading chart…"
  label-empty="No data to display"
  label-plot="Chart data. Use the arrow keys to move between slices, Home and End for the first and last, Escape to leave."
  label-point="%label%: %value% (%percent%)"
  animation="expressive|grow|fade|draw|stagger|none"   <!-- default: expressive -->
  animation-duration="700"                       <!-- default: engine default -->
  no-animation                                   <!-- default: off -->
  density="-1|-2|-3|-4"                          <!-- default: 0 (uncompacted) -->
>
  <div slot="center"><strong>12,480</strong><br>visits</div>
</md-pie-chart>
```

Arrays and functions have no attribute form — set them as JS properties:

```js
const chart = document.querySelector('md-pie-chart');

chart.data = [
  { label: 'Organic',  value: 5200 },
  { label: 'Direct',   value: 3900, color: 'tertiary' },
  { label: 'Referral', value: 3380, children: [       // becomes a second ring
    { label: 'Blogs', value: 2000 },
    { label: 'Forums', value: 1380 },
  ] },
];
chart.ringWidths     = [2, 1];                        // inner ring twice as wide
chart.tableLabels    = { category: 'Source', value: 'Visits', share: 'Share' };
chart.valueFormatter = (v) => new Intl.NumberFormat('en-US').format(v);
chart.tooltipRenderer = (ctx) => {
  const hovered = ctx.series.find((s) => s.focused);
  return `${ctx.axisLabel}: ${hovered?.formattedValue ?? ''}`;
};
```

Each datum (`MdPieDatum`): `label` and `value` are required; `color`, `id`,
`selected` (drawn exploded), `hidden`, `radius` (0–1, a second dimension) and
`children` (a nested ring) are optional.

**Events** — `mdSliceClick` (`MdChartClickDetail<MdPieDatum>`), `mdLegendClick`
(`{ dataIndex, selected }` — note this differs from the other charts, which
report `seriesIndex`), `mdReady`. All are the Stencil default — they bubble and
cross shadow boundaries. There is **no `mdHover` and no `mdZoom`** here.

**Methods** — `resize()`, `replay()`, `drill(index, direction?)`,
`toDataURL()`, `getInstance()`. All are async. There is no `setZoom()` /
`resetZoom()` — a ring has no span to zoom.

**Slots** — `header` (above the chart), `center` (inside the donut hole),
`footer` (below), `empty` (replaces `label-empty`), and `loader` (replaces the
built-in spinner; `loading` is the older alias for the same slot — `loader`
wins if both are filled).

**Parts** — `header`, `canvas`, `center`, `empty`, `loading`, `footer`. The
engine additionally marks the `<canvas>` as `plot-canvas` and the DOM overlay's
legend and hover card as `legend` and `tooltip`.

### Behavioral contract worth knowing

- **The data prop is `data`, not `series`** — unlike the other four charts. It
  is a flat array of `{ label, value }` objects, set as a JS property.
- The **`center` slot only makes sense with a donut** (`inner-radius` > 0); with
  a solid pie the content sits over the slices. It is `aria-hidden="true"`, so
  whatever it says must also exist in the surrounding copy.
- **`show-labels` is ON by default** here (the other charts default it off).
  `label-mode` is undefined by default and resolves **per ring**, not once for
  the chart:
  - single ring **with** a legend → `value`, drawn inside the slice;
  - single ring **without** a legend → `both` (`name · value`), outside on a
    leader line;
  - nested rings → the innermost ring labels as `name` (inside), the outermost
    as `both` (outside, on a leader), and the rings in between are not labelled
    at all — there is no room in the band.

  Setting `label-mode` explicitly overrides all of that, everywhere.
- **`tooltip` is inert on this component.** The prop exists for parity with the
  other four charts, but nothing reads it: the hover card is always on, and
  `tooltip="none"` will **not** suppress it. Change what the card says with
  `tooltipRenderer`; there is no switch to turn it off.
- **`monochrome` present with no value means `primary`.** Absent means off — so
  `monochrome=""` is the "on, with the default" case, not an accident.
- A slice's `children` are drawn as a further ring subdividing exactly that
  slice's arc, to arbitrary depth. Children are taken in proportion to each
  other, so they need not add up to the parent's value. `ringWidths` (relative
  weights, innermost first) divides the radial band between rings.
- **Legend toggles survive a data re-feed**; setting `hidden` on a datum takes
  visibility back from the reader. A hidden slice keeps its legend chip (struck
  through) but contributes nothing to the ring.
- **`drill()` must be awaited before the swap.** Like every Stencil `@Method` it
  resolves through a microtask, so a bare call lands *after* an assignment on the
  next line and the swap renders un-animated:
  `await chart.drill(i); chart.data = next;` For `'up'`, the index names a slice
  of the data about to be shown.
- `start-angle` / `end-angle` default to `90` / `-270` — a full circle starting
  at the top. A 180° span gives a gauge look.
- Slices assume **non-negative** values that sum to a meaningful whole.
- `label-point` uses **`%label%`, `%value%` and `%percent%`** — note these differ
  from the other charts' `%x%` / `%values%`.
- Call `resize()` after a container reveal; gate `toDataURL()` on `mdReady`.
- `getInstance()` returns the underlying engine. It is an escape hatch; anything
  done through it is outside this component's contract.
- Charts re-read the theme tokens and repaint on their own when
  `prefers-color-scheme` changes. Any *other* theme swap (a manual light/dark
  class, a brand-token change) needs a nudge: reassign `data`. `resize()` will not
  do it — it returns early when the box has not changed.

---

## Do / Don't

House rules — M3 ships no chart component, so the guidance below is this
library's own.

| ✅ Do | ❌ Don't |
|---|---|
| Keep to about 2–6 slices | Don't render a dozen slivers — use a bar chart |
| Group the tail into "Other" | Don't show fifteen 1% slices |
| Use a donut with a centre total when the total matters | Don't put content in the centre of a solid pie |
| Order slices by size (largest first) | Don't use an arbitrary order |
| Label slices or provide a legend | Don't rely on colour alone |
| Use it only when values sum to a whole | Don't pie-chart unrelated quantities |
| Use a bar chart when precise comparison matters | Don't ask readers to compare similar angles |
| Honour reduced motion | Don't force the entrance animation |
| Provide a data table alternative | Don't make the chart the only access |

---

## Patterns

```html
<md-pie-chart id="c" label="Traffic by source" inner-radius="60%" height="320px">
  <div slot="center"><strong>12,480</strong><br>visits</div>
</md-pie-chart>

<script type="module">
  const c = document.getElementById('c');
  c.data = [
    { label: 'Organic',  value: 5200 },
    { label: 'Direct',   value: 3900 },
    { label: 'Referral', value: 2100 },
    { label: 'Other',    value: 1280 },
  ];
  c.valueFormatter = (v) => new Intl.NumberFormat('en-US').format(v);
  c.addEventListener('mdReady', () => console.log('drawn'));
</script>
```

```html
<!-- Drill into a slice -->
<md-pie-chart id="d" label="Traffic by source"></md-pie-chart>
<script type="module">
  const d = document.getElementById('d');
  d.data = [
    { label: 'Organic', value: 5200, id: 'organic' },
    { label: 'Direct',  value: 3900, id: 'direct' },
  ];

  d.addEventListener('mdSliceClick', async (e) => {
    await d.drill(e.detail.dataIndex);          // await, THEN swap
    d.data = [
      { label: 'Search', value: 3100 },
      { label: 'Images', value: 2100 },
    ];
  });
</script>
```

```html
<!-- Labels on the slices -->
<md-pie-chart show-labels label-mode="both"></md-pie-chart>

<!-- Single-hue, separated slices -->
<md-pie-chart monochrome padding-angle="2"></md-pie-chart>

<!-- Gauge-style half donut -->
<md-pie-chart inner-radius="70%" start-angle="180" end-angle="0"></md-pie-chart>

<!-- Async -->
<md-pie-chart loading loading-label="Loading traffic…"></md-pie-chart>
```

## Anti-patterns

| ❌ Wrong | ✅ Right | Why |
|---|---|---|
| `chart.series = [...]` | `chart.data = [...]` | Pie uses `data`, unlike the other charts. |
| `<md-pie-chart data='[…]'>` | Assign the array in JS | Arrays don't cross the attribute boundary. |
| `center` slot on a solid pie | Set `inner-radius` first | Content would sit over the slices. |
| Relying on the `center` slot to announce the total | Repeat it in page copy | The centre overlay is `aria-hidden`. |
| `chart.drill(i); chart.data = next;` | `await chart.drill(i)` first | The method resolves a microtask later, so the swap renders un-animated. |
| `e.detail.seriesIndex` on `mdLegendClick` | `e.detail.dataIndex` | Pie's legend detail is `{ dataIndex, selected }`. |
| Listening for `mdHover` / calling `setZoom()` | Neither exists here | Pie has no hover event and no zoom. |
| Twelve slices | Group into "Other", or use `md-bar-chart` | Slivers are unreadable. |
| `<md-pie-chart tooltip="none">` | Nothing — the hover card can't be turned off here | `tooltip` is declared but never read on this component. |
| Negative values | `md-bar-chart` | Angles can't encode sign. |
| `%x%` in `label-point` | `%label%` / `%value%` / `%percent%` | Different tokens from the other charts. |
| `toDataURL()` before `mdReady` | Wait for the event | Nothing drawn yet. |
| Chart in a hidden tab with no `resize()` | Call it on reveal | Renders at zero size. |

## Accessibility, RTL, density, i18n

**Accessibility**
- The host is `role="figure"` with a generated `aria-label` summary (`summary`
  replaces it wholesale), and the chart is a focusable `role="application"`
  region named by `label-plot`. Arrow keys move a keyboard cursor between
  slices, Home/End jump to the ends, Escape leaves; each move is announced
  through a polite live region built from `label-point`.
- A screen-reader-only data table is rendered inside the component;
  `tableLabels` translates its `category` / `value` / `share` headings.
- Pie charts are the hardest chart type to read non-visually **and** visually —
  **always** offer the numbers as a table or list.
- The `center` slot is `aria-hidden="true"`; anything it says must be in the
  page text too.
- Never distinguish slices by colour alone: use `show-labels` with
  `label-mode="both"`, or a legend with values.
- `monochrome` produces a single-hue ramp, which is harder to tell apart —
  pair it with labels.
- `loading` sets `aria-busy="true"` and names the overlay with `loading-label`.
- The engine already honours `prefers-reduced-motion`; `no-animation` turns the
  entrance off outright.

**RTL** — the header, footer and centre overlay are DOM and follow `dir`. The
ring itself is **not** mirrored: slice order and sweep direction stay as
configured, which is correct for a shape with no reading direction. The legend
**is** mirrored, and unlike the cartesian charts it mirrors *every* anchor,
physical ones included. Under `dir="rtl"`, `legend="right"` lays out on the
physical **left**, `legend="left"` on the right, `top-start` becomes `top-end`,
`bottom-end` becomes `bottom-start`; plain `top` / `bottom` have no side to
swap. (`md-bar-chart`, `md-line-chart` and `md-area-chart` differ: there
`left` / `right` keep the side they name.)

**Density** — `density="-1"` through `density="-4"` tighten the padding, corner
radius and minimum height. Rung `0` is the uncompacted default and has no rule
of its own. To step *out* of an inherited `data-density` rung, set
`style="--md-sys-density-scale: 0"` — `density="0"` will not do it.

**i18n** — set `locale` for the default number formatting, or take it over with
`valueFormatter`, which always wins. Translate `label`, `subtitle`, `summary`,
`label-empty`, `loading-label`, `label-plot`, `label-point` (keeping `%label%` /
`%value%` / `%percent%`) and `tableLabels`.

## Related components

`md-bar-chart` · `md-area-chart` · `md-line-chart` · `md-sparkline` ·
`md-progress-indicator`

## Theming

| Custom property | Purpose | Default |
|---|---|---|
| `--md-pie-chart-block-size` | Explicit chart height | `auto` |
| `--md-pie-chart-min-block-size` | Floor the height never drops below | `max(140px, 200px + density × 12px)` |
| `--md-pie-chart-aspect-ratio` | Ratio used when no block-size is set | `1 / 1` (square) |
| `--md-pie-chart-background` | Chart surface fill | `--md-sys-color-surface-container-low` |
| `--md-pie-chart-padding` | Inset between host edge and canvas | `max(8px, 16px + density × 2px)` |
| `--md-pie-chart-shape` | Corner radius of the chart surface | `max(8px, 16px + density × 2px)` |
| `--md-pie-chart-center-color` | Text colour of the donut centre | `--md-sys-color-on-surface` |
| `--md-pie-chart-empty-color` | Empty-state text colour | `--md-sys-color-on-surface-variant` |
| `--md-pie-chart-empty-background` | Empty-state overlay fill | the chart background |
| `--md-pie-chart-empty-font` | Empty-state font family | body-medium family |
| `--md-pie-chart-empty-font-size` | Empty-state font size | body-medium size (14px) |
| `--md-pie-chart-empty-icon-size` | Icon slotted into the empty state | `40px` |

Slice colours come from the MD3 palette, not from these properties: set a
datum's `color` to an MD3 role (`'primary'`, `'tertiary'`, `'success'`, …) or
any CSS colour, or switch the whole ring to one hue with `monochrome`.

**CSS parts** — `header`, `canvas`, `center`, `empty`, `loading`, `footer`,
plus the engine-set `plot-canvas`, `legend` and `tooltip`.

```css
md-pie-chart {
  --md-pie-chart-background: transparent;
  --md-pie-chart-center-color: var(--md-sys-color-primary);
}
```

<!-- Auto Generated Below -->


## Properties

| Property            | Attribute            | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | Type                                                                                                             | Default                                                                                                          |
| ------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `animation`         | `animation`          | Entry-animation variant: `expressive` (default), `grow`, `fade`, `draw`, or `none`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | `"draw" \| "expressive" \| "fade" \| "grow" \| "none" \| "stagger"`                                              | `'expressive'`                                                                                                   |
| `animationDuration` | `animation-duration` | Entry-animation duration override in ms (≤ 0 disables).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | `number \| undefined`                                                                                            | `undefined`                                                                                                      |
| `cornerRadius`      | `corner-radius`      | Border radius applied to each slice (reserved).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `number`                                                                                                         | `4`                                                                                                              |
| `data`              | --                   |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `MdPieDatum[]`                                                                                                   | `[]`                                                                                                             |
| `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`                                                                                                              |
| `endAngle`          | `end-angle`          | End angle in degrees.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | `number`                                                                                                         | `-270`                                                                                                           |
| `gradient`          | `gradient`           | Fill each slice with a gradient running outward from the centre, for depth. Purely decorative — it changes nothing about what a slice means, which is why it is off by default.                                                                                                                                                                                                                                                                                                                                                                                                                         | `boolean`                                                                                                        | `false`                                                                                                          |
| `heightProp`        | `height`             |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `string \| undefined`                                                                                            | `undefined`                                                                                                      |
| `highlight`         | `highlight`          | Hover highlight scope: slice / series / none.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           | `"none" \| "series" \| "slice"`                                                                                  | `'slice'`                                                                                                        |
| `innerRadius`       | `inner-radius`       | Inner radius (`"0%"` = pie, `"60%"` = donut, or px number).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | `number \| string`                                                                                               | `'0%'`                                                                                                           |
| `label`             | `label`              |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `string`                                                                                                         | `''`                                                                                                             |
| `labelEmpty`        | `label-empty`        | Message shown when `data` is empty. The `empty` slot overrides it.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `string`                                                                                                         | `'No data to display'`                                                                                           |
| `labelMode`         | `label-mode`         | What each slice's label says: its `value`, its `name`, or `both`. Defaults to the value when a legend is present (it already names the slices) and to both when there isn't one.                                                                                                                                                                                                                                                                                                                                                                                                                        | `"both" \| "name" \| "value" \| undefined`                                                                       | `undefined`                                                                                                      |
| `labelPlot`         | `label-plot`         | Instructions announced when the plot receives keyboard focus. The plot is focusable so a keyboard user can walk the slices; this is what tells them.                                                                                                                                                                                                                                                                                                                                                                                                                                                    | `string`                                                                                                         | `'Chart data. Use the arrow keys to move between slices, Home and End for the first and last, Escape to leave.'` |
| `labelPoint`        | `label-point`        | Template for the live announcement as focus moves. `%label%` is the slice name, `%value%` its formatted value and `%percent%` its share.                                                                                                                                                                                                                                                                                                                                                                                                                                                                | `string`                                                                                                         | `'%label%: %value% (%percent%)'`                                                                                 |
| `legend`            | `legend`             |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `"bottom" \| "bottom-end" \| "bottom-start" \| "left" \| "none" \| "right" \| "top" \| "top-end" \| "top-start"` | `'right'`                                                                                                        |
| `loading`           | `loading`            | Show the loading overlay instead of the chart.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | `boolean`                                                                                                        | `false`                                                                                                          |
| `loadingLabel`      | `loading-label`      | Text (and the spinner's accessible name) for the loading overlay.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       | `string`                                                                                                         | `'Loading chart…'`                                                                                               |
| `locale`            | `locale`             | BCP-47 locale for the DEFAULT number formatting (tooltip values, the screen-reader table). Empty follows the browser, which is the right default only until the page has said otherwise; an explicit `valueFormatter` always wins over it.                                                                                                                                                                                                                                                                                                                                                              | `string`                                                                                                         | `''`                                                                                                             |
| `monochrome`        | `monochrome`         | Render every slice as a shade of ONE colour instead of the categorical palette — darkest first, lightening around the ring.  Worth reaching for when the slices are ordered (a ranking, a funnel) rather than merely different: a single hue stops the reader hunting for meaning in colour that isn't there, and it survives most colour-vision deficiencies, which a five-hue palette does not.  Takes an MD3 role or any CSS colour. Present with no value (`monochrome`) uses `primary`; absent means off — so an explicit empty string is the "on, with the default" case rather than an accident. | `string \| undefined`                                                                                            | `undefined`                                                                                                      |
| `noAnimation`       | `no-animation`       | Disable all animation (shorthand for `animation="none"`).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | `boolean`                                                                                                        | `false`                                                                                                          |
| `outerRadius`       | `outer-radius`       | Outer radius. Defaults to `"75%"`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `number \| string`                                                                                               | `'75%'`                                                                                                          |
| `paddingAngle`      | `padding-angle`      | Padding angle (degrees) between adjacent slices.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | `number`                                                                                                         | `0`                                                                                                              |
| `ringWidths`        | --                   | How the radial band is divided between the rings of a nested chart — relative weights, innermost first. `[2, 1]` gives the inner ring twice the width of the outer one.  The default is not an even split: every ring but the outermost puts its labels INSIDE its own band, so it has to be wide enough to hold a word, while the outermost labels on leaders and needs no more than its arc.                                                                                                                                                                                                          | `number[] \| undefined`                                                                                          | `undefined`                                                                                                      |
| `showLabels`        | `show-labels`        | Show slice labels around the ring.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `boolean`                                                                                                        | `true`                                                                                                           |
| `startAngle`        | `start-angle`        | Start angle in degrees (90 = 12-o'clock).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | `number`                                                                                                         | `90`                                                                                                             |
| `subtitle`          | `subtitle`           | Sub-title, drawn under the title in the muted text colour.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | `string \| undefined`                                                                                            | `undefined`                                                                                                      |
| `summary`           | `summary`            | Replaces the generated `aria-label` outright. The default summary is assembled in English; rather than translate it piecewise, hand over the whole sentence built in your own language.                                                                                                                                                                                                                                                                                                                                                                                                                 | `string`                                                                                                         | `''`                                                                                                             |
| `tableLabels`       | --                   | Translatable chrome for the screen-reader data table.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | `undefined \| { category?: string \| undefined; value?: string \| undefined; share?: string \| undefined; }`     | `undefined`                                                                                                      |
| `titleAlign`        | `title-align`        | Title alignment over the chart: `start` (left), `center`, or `end` (right).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | `"center" \| "end" \| "start"`                                                                                   | `'start'`                                                                                                        |
| `tooltip`           | `tooltip`            |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `"axis" \| "item" \| "none"`                                                                                     | `'item'`                                                                                                         |
| `tooltipRenderer`   | --                   | Replace the tooltip's content. See `md-line-chart`'s `tooltipRenderer`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | `((context: MdChartTooltipContext) => MdChartTooltipContent) \| undefined`                                       | `undefined`                                                                                                      |
| `valueFormatter`    | --                   |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `((value: number) => string) \| undefined`                                                                       | `undefined`                                                                                                      |


## Events

| Event           | Description | Type                                                     |
| --------------- | ----------- | -------------------------------------------------------- |
| `mdLegendClick` |             | `CustomEvent<{ dataIndex: number; selected: boolean; }>` |
| `mdReady`       |             | `CustomEvent<void>`                                      |
| `mdSliceClick`  |             | `CustomEvent<MdChartClickDetail<MdPieDatum>>`            |


## Methods

### `drill(index: number, direction?: "down" | "up") => Promise<void>`

Animate the next `data` change as a drill through one slice, so the levels
of a hierarchy read as connected rather than as unrelated charts.

AWAIT it, then assign the new `data`. Like every Stencil `@Method` this one
resolves through a microtask, so a bare call lands AFTER a `data` assignment
on the next line — the swap renders un-animated and the drill arms itself
for whatever change comes after.

```ts
await chart.drill(e.detail.dataIndex);          // descend into the clicked slice
chart.data = childrenOf(clicked);

await chart.drill(indexOfChildInParent, 'up');  // …and back out of it
chart.data = parentLevel;
```

`down` names a slice of the data currently shown; `up` names a slice of the
data about to be shown — in both cases, the wedge that connects the two
levels. The new ring unfurls out of it, which run in reverse is what makes
coming back up read as zooming out of that same wedge. Falls back to a plain
swap when the index doesn't resolve, or under `prefers-reduced-motion`.

#### Parameters

| Name        | Type             | Description |
| ----------- | ---------------- | ----------- |
| `index`     | `number`         |             |
| `direction` | `"up" \| "down"` |             |

#### Returns

Type: `Promise<void>`



### `getInstance() => Promise<PieChartEngine | null>`



#### Returns

Type: `Promise<PieChartEngine | null>`



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

Replay the entry animation from the start (uses the current `animation`).

#### Returns

Type: `Promise<void>`



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



#### Returns

Type: `Promise<void>`



### `toDataURL() => Promise<string>`



#### Returns

Type: `Promise<string>`




## Shadow Parts

| Part        | Description |
| ----------- | ----------- |
| `"canvas"`  |             |
| `"center"`  |             |
| `"empty"`   |             |
| `"footer"`  |             |
| `"header"`  |             |
| `"loading"` |             |


## Dependencies

### Depends on

- [md-progress-indicator](../md-progress-indicator)

### Graph
```mermaid
graph TD;
  md-pie-chart --> md-progress-indicator
  style md-pie-chart 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.