design-system prose: rationale, usage guidance, gotchas, patterns, and token narratives. It explains how to edit that content now that Helix lives in one monorepo.
What changed (the cutover). Helix content used to live in a separate
helix-design-systemrepo. The DSM edited markdown there, and it was hand-ported into the component library by an engineer — a slow, drift-prone two-repo dance. That's gone. The canonical content now lives here incontent/, is edited in place, and the viewer renders it directly. No porting, no second repo.
The two-layer model (read this first)
Every fact about the design system has exactly one owner. Knowing which layer owns a fact tells you where to edit it.
| Layer | Lives in | Owned by | Holds |
|---|---|---|---|
| Structured facts | packages/helix-ui/src/metadata/*.metadata.ts | Engineering | Variants, props, anti-patterns, a11y attributes, token bindings — anything a machine parses. Feeds the MCP server and the viewer's auto-rendered "Spec" tables. |
| Prose & guidance | content/ (this directory) | You (DSM) | Rationale, when-to-use, gotchas, patterns, token narratives — the human story. |
The viewer merges both at render time: it draws the structured "Spec" panel from metadata and renders your markdown around it.
[!IMPORTANT] Don't restate structured fields in prose. The variant list, props table, and a11y attributes are rendered automatically from metadata. If you also type them into the markdown, the two will drift. Write the why and the how, not the what — the table already shows the what.
A CI gate (verify:docs, below) fails the build if the layers fall out of sync,
so neither side can ship a change that silently breaks the other.
Where content lives
content/ is flattened — no system/ nesting (an artifact of the old repo
that's been removed). Each subdirectory maps to a section of the viewer:
| Directory | Holds |
|---|---|
components/ | One page per component (58 of them) + component-template.md for new specs |
tokens/ | Color, typography, spacing, radii, shadows, motion, breakpoints + machine-readable tokens.json |
patterns/ | Cross-component patterns (component-match, layouts, filtering, table-behavior) |
rules/ | Hard rules: accessibility, tech-stack, project-structure, governance |
a11y/ | Accessibility requirements by component category |
decisions/ | Recorded design decisions + rationale |
icons/ | Icon inventory and usage |
tasks/ | Task playbooks AI agents load (create-component, review-ui, etc.) |
The loader is apps/docs/src/lib/system.ts:
it reads content/<category>/<slug>.md directly, using each file's # H1 (or
frontmatter) as the title. A README.md in a folder becomes that section's
"Overview."
The authoring loop
edit content/*.md → npm run docs:dev → see it instantly → verify → commit
- Edit the relevant markdown file in
content/. - Preview — from the repo root, run
npm run docs:devand open the local viewer. It readscontent/live; your edits show on save. No build step, no porting. - Verify — run
npm run verify:docs(see below) before committing. - Commit on a branch and open a PR. CI re-runs the gate.
[!TIP] Writing rationale, gotchas, and guidance is your highest-value work — it's the layer no metadata table can capture. Lean into the why.
The no-drift gate (verify:docs)
scripts/verify-docs.mjs enforces the two-layer contract. Run it with
npm run verify:docs (it needs the package built first — npm run build).
It fails the build when:
- Coverage — a component exists in metadata but has no
content/components/page, or vice versa (minus a small list of declared exceptions, like the template). - Variant drift — a
variantdeclared in a component's metadata isn't mentioned anywhere in that component's markdown. (So when engineering adds a variant in code, the docs must be updated before it can ship.)
It warns (but won't block) when: a metadata token binding references a token
missing from tokens.css, or a component slug is missing from figma-map.json
/ registry.json.
What this means for you: if you add a new component page, engineering must add its metadata in the same change (or the coverage check fails) — coordinate the two. If you rename or remove a component's variant in prose, make sure it still names every variant metadata declares.
Adding a new component spec
- Copy
components/component-template.mdtocontent/components/<slug>.md. - Write the prose — rationale, when-to-use, composition guidance, gotchas. Leave the structured facts (variants/props/a11y) to metadata; the Spec panel renders them.
- Coordinate with engineering so the matching
packages/helix-ui/src/metadata/<slug>.metadata.tslands in the same PR — otherwiseverify:docsfails on coverage. - Mention every variant metadata declares (so the variant-drift check passes).
npm run verify:docs, then PR.
For proposing a brand-new component or pattern (not just documenting one), use
the governance process in rules/governance.md and the
rules/proposal-template.md.
What you own vs. what engineering owns
You edit (here in content/) | Engineering edits (in packages/helix-ui/) |
|---|---|
| Usage guidance, when-to-use, rationale | Component code (src/components/) |
| Gotchas, anti-pattern narratives | Structured metadata (src/metadata/*.metadata.ts) |
| Token narratives & design intent | Token values (src/tokens/, tokens.css) |
| Patterns, decisions, a11y prose | Variants, props, a11y attributes (in metadata) |
When in doubt: if a machine needs to parse it deterministically, it's metadata (engineering); if a human needs to understand the reasoning, it's prose (you).
Single brand, light + dark. Edit in place, preview live, let the gate keep the two layers honest.