Skip to content

Component page rubric

Every page under /components/<name> follows the same shape. The shape isn't decoration — it's the contract that lets readers move between components without re-learning where to look, lets agents parse pages predictably, and lets the drift gate fail CI when a section is missing.

The reference implementation is Pill. If a section here is unclear, open Pill and read how it's done.

Frontmatter contract

---
title: Button                                   # ← required, the export name
description: "One-line description, sentence-cased, ends with a period."
audience: [designer, developer, agent]          # ← always all three
component: Button                                # ← matches `title`
source: packages/components/src/Button          # ← path to the source
status: stable                                   # ← stable | beta | deprecated
category: primitives                             # ← primitives | surfaces | domain | composer | headings | brand
relatedComponents: [Pill, MatterButton]          # ← cross-link siblings
a11y:
  keyboard: "Enter and Space activate. Tab moves focus."
  screenReader: "Announced as the button's accessible name plus its disabled / pressed state."
  focus: "Canonical peach ring via --focus-ring."
  contrast: "AA against every surface token in the contract."
  reducedMotion: "Hover lift collapses to opacity transition."
  forcedColors: "Border falls back to ButtonText, label to ButtonFace."
---

Frontmatter fields are surfaced in the generated design-system.json, render in the page header (status: becomes a pill, relatedComponents: becomes a sidebar block), and feed the per-component accessibility matrix at foundations/a11y.

The fifteen sections

In this exact order. Skip any section whose content is genuinely "n/a" and leave a one-line note saying why — never just delete the heading.

1. Hero example

One <ComponentPreview component="Name"> or interactive demo at the top, no caption. This is what someone reading the page will see in their tab preview / OG card.

2. When to use

One to two paragraphs. Ground it in Matter's narrative — entity-as-object, the Create / Manage / Exit lifecycle, the agent token tiers. Include two patterns:

  • Reach for X when — the canonical situation. Be specific. "Reach for Button when an action will mutate state on the server." Not "Use Button for buttons."
  • Don't reach for X when — the situation where a sibling component is the right call. Link to the sibling.

Close with a one-line <DoDont> pair.

3. Anatomy

A labelled diagram of the component's structural parts. Inline SVG with text callouts is the canonical pattern; a screenshot with annotations is fine when the part labels need pixel-precise placement.

The point of this section: a designer can point at a slot and read its name back. A developer can guess the prop without scrolling.

4. Variants / tones

If the component has variants (primary / secondary / ghost) or tones (neutral / green / orange), a table maps each to:

  • the tokens consumed
  • the use case
  • the V1 / legacy component it replaces (if any, important during migrations)

Pill's tone matrix is the model.

5. Props

<PropsTable component="Name" />

Auto-rendered from the introspected .props.json. Don't hand-author this section — author the JSDoc on the source instead and re-run bun run --filter design generate:propstable. The drift gate enforces that every component has an introspected props file.

6. States

<StatesGrid component="Name" />

Plus a short paragraph naming each state and saying when it triggers (hover, focus, active, disabled, loading, error, empty, success). If a state is genuinely unreachable for this component, omit it from the grid and note why.

7. Density

<DensitiesGrid component="Name" />

Plus one paragraph on what the compact density removes (vertical rhythm, padding, line-height) and what it never removes (touch targets, focus rings, contrast).

8. Themes

<ComponentPreview component="Name" theme="light" />
<ComponentPreview component="Name" theme="dark" />
<ComponentPreview component="Name" theme="forced-colors" />

Three previews side-by-side. The point is to prove parity, not just illustrate — every page that ships passes this row.

9. Tokens consumed

<TokenTable kind="colors" filter="name" />

Filter to the tokens this component actually reads. If the component spans multiple kinds (colors + radii + motion), repeat the block per kind.

10. Accessibility

Replace the generic "keyboard / aria / focus / reduced-motion" stub with concrete per-component notes:

  • Keyboard interactions. Specific keys and behaviour. "Enter activates. Escape closes the popover. Arrow keys move between options."
  • ARIA role and properties. What role the element carries, what state attributes flip when.
  • Focus order. Where focus lands on mount, where it returns on close.
  • Screen-reader expectations. What text is announced, in what order, on what state change.
  • Reduced motion. What collapses, what stays.
  • Forced colors. What the system replaces, what falls back to system colors.
  • WIG rules touched. Cross-link the Web Interface Guidelines rules this component implements (icon-only aria-label, semantic element, hit-target, touch-action, etc.).

11. Do / Don't

At least three real <DoDont> pairs. "A short positive example" / "A short negative example" doesn't count.

<DoDont>
  <div data-good>**Do** — render Pill inline with the entity ID so the chip reads as an annotation, not a control.</div>
  <div data-bad>**Don't** — wrap Pill in a clickable element. It's a label, not a button.</div>
</DoDont>

12. Recipes

Link to every /recipes/* page where this component appears in a real screen. If it appears in zero recipes today, write one sentence explaining the most likely first use.

13. Code example

A concrete <CodeCard> block with realistic usage — not a generic prop list, not a "Hello World." Use Matter's real domain (entities, filings, grants).

import { Pill } from "@matter/components";

export function FilingStatus({ filing }: { filing: Filing }) {
  return (
    <Pill icon="✓" tone={filing.state === "active" ? "green" : "neutral"}>
      {filing.state}
    </Pill>
  );
}
<RelatedComponents />

Auto-rendered from frontmatter relatedComponents:. Land here when the reader's next question is likely "what's the sibling component?" — every page benefits from this even if the answer is just one entry.

15. Source

<SourceLink path="packages/components/src/Pill" />
<FigmaLink node="123:4567" />

Both links required. The Figma node ID comes from selecting the canonical component in the Matter Figma file and copying the node ID from the share menu.

Voice rules

The authoring rules and voice pages own the canonical guidance. Two reminders that matter most in component docs:

  1. Never compare to other products. No "Matter's analog of X", no "deliberate deviation from Y", no "mirrors Z's grammar." Describe what the component does plainly. Factual references to standards (WAI-ARIA roles, RFC 7807) are fine; framing Matter's design as a comparison to a named competitor is not.
  2. Active voice, second person. "You'll reach for Pill when…" not "Pill is reached for when…".

What the drift gate enforces

The CI gate at check-design-drift.ts fails on:

  1. An exported component in packages/components/src/index.ts (or the brand index) with no MDX page under /components/<kebab>.mdx.
  2. An MDX page that references a token (CSS variable) that no longer exists in @matter/tokens.
  3. An MDX page that contains a raw hex / rgba() color literal outside a code fence, <DoDont> block, or table row.
  4. An exported component with no introspected props file under .generated/components/<Name>.props.json.

Future tightening (planned, see the audit plan): the gate will also fail on missing rubric headings, on placeholder strings like "Replace this description when the page is hand-filled", and on competitor-product comparisons.

Scaffolding a new page

bun run --filter design generate:component-pages

Generates the 76-line scaffold for any exported component that lacks an MDX page. Then hand-fill against the fifteen sections above. The scaffold is a starting point — every section needs human authorship before the page is considered shipped.

On this page