ADR 0009 — Semantic typography naming
Status
Accepted, 2026-05-18.
Source: v1.0 architectural quiz outcomes
(docs-internal/architecture/v1-master-diff-merge.md §3.4, §6.2).
Context
v0.1.0 used abstract typography sizes: font-heading-l,
font-heading-m, font-heading-s, font-body-l, font-body-m,
font-body-s, font-body-xs. The t-shirt naming has well-known
problems that compound in an AI-readable system:
- Size names lie about role. "Heading-l" is a heading at a large
size — but the typographic role of a hero headline on a landing
page is materially different from the role of an H1 inside an
article. Both rendered at large sizes; only one is a "display"
typography. Treating them as variants of the same
font-heading-lblurs the design intent. - AI agents over-pick the largest available size. When asked for
"the main headline," an agent reading
font-heading-l/font-heading-mwill choose-lby default. The actual decision should hinge on the semantic role (hero vs. page title), not the visual size. - Fluid sizing has no natural home. Some typography should scale fluidly with viewport (hero, statement) while most should not (body copy, captions). The size-based naming had no axis to encode this.
- Theme variation gets stuck. A future "Malevich Editorial" theme
might use materially different scales for display vs. body. Under
size-based naming, the editorial theme would have to invent
font-heading-2xlor break the API.
The v0.1.0 set works fine for the ten components shipped. It scales poorly to the 53 components and richer page archetypes (landing heroes, marketing CTAs, documentation portal) in v1.0.
Decision
Adopt role-based semantic typography names, organized into four groups by purpose:
Group: display (fluid)
The largest typography. Intended for above-the-fold and full-bleed
text. Sizes scale fluidly via clamp().
| Token | Role | Size |
|---|---|---|
display.hero |
Landing hero, maximum impact | clamp(2.5rem, 1.5rem + 5vw, 5rem) |
display.statement |
Section-level statement | clamp(2rem, 1.5rem + 3vw, 3.5rem) |
display.title |
Page title emphasis | clamp(1.75rem, 1.25rem + 2vw, 2.5rem) |
Group: heading (fixed)
In-document headings. Mapped to HTML h1-h4 by default; not all
H1s are headings in this sense (a landing-page H1 may be display.hero).
| Token | Role | Size |
|---|---|---|
heading.title |
Main heading, default H1 inside content | 1.875rem / 30px |
heading.section |
Section heading, default H2 | 1.5rem / 24px |
heading.subsection |
Subsection heading, default H3 | 1.25rem / 20px |
heading.group |
Group heading, default H4 | 1.125rem / 18px |
Group: body (fixed)
Reading text.
| Token | Role | Size |
|---|---|---|
body.lead |
Lead paragraph, emphasized intro | 1.125rem / 18px |
body.regular |
Standard body copy | 1rem / 16px |
body.support |
Supporting text (helper text, captions in flow) | 0.875rem / 14px |
Group: special (fixed)
Edge-case typography that doesn't fit display/heading/body.
| Token | Role | Size |
|---|---|---|
caption |
Figure captions, small annotations | 0.75rem / 12px |
overline |
Eyebrow labels, ALL CAPS, wide tracking | 0.6875rem / 11px |
eyebrow |
Mixed-case overline | 0.75rem / 12px |
code |
Inline code | 0.875rem / 14px, monospace |
mono.l / mono.m / mono.s |
Block monospace | 16 / 14 / 12 px |
l/m/s survives only in the mono set, where the three sizes have
no role distinction — purely block scale.
Why role-based, not size-based
- Role names self-document intent. A template using
font-display-herosignals "this is a hero" to readers, designers, and AI agents.font-heading-xlonly says "it's big." - Fluid scaling becomes a property of the role. Display typography is fluid; body is fixed. The naming encodes this distinction.
- Theming has room. "Malevich Editorial" can re-tune the scale of
every role without renaming. Display-hero stays display-hero even
if it goes from
5remto4rem. - AI agents pick by intent, not size. "What's the right token for
the landing page headline?" →
font-display-hero, unambiguous. - HTML tag mapping stays default-correct.
<h1>inside<main>getsfont-heading-titleautomatically (via adjacent-selector rules); landing pages override tofont-display-heroexplicitly.
Migration from v0.1.0
| v0.1.0 token | v1.0 token | Notes |
|---|---|---|
font-heading-hero |
font-display-hero |
Was already role-named informally |
font-heading-section |
font-display-statement |
Renamed |
font-heading-block |
font-display-title |
Renamed |
font-title-l |
font-heading-title |
Renamed |
font-title-m |
font-heading-section |
Renamed |
font-title-s |
font-heading-subsection |
Renamed |
font-subheading-l/m/s |
font-heading-group (collapsed) |
One subheading role suffices |
font-body-l |
font-body-lead |
Renamed |
font-body-m |
font-body-regular |
Renamed |
font-body-s |
font-body-support |
Renamed |
font-body-xs |
font-caption |
Promoted to special |
font-action-l/m/s |
(removed) | Buttons consume body tokens; size variants come from rhythm |
font-overline-l/m/s |
font-overline (single value) |
Single canonical overline |
font-mono-m |
font-mono-m |
Kept; mono retains l/m/s |
The v0.2.0 migration codemod maps old → new names in CSS files. A
small handful of CSS rules using font-action-* need manual review
because their replacement depends on intent.
Fluid scaling enforced at the token level
clamp() lives in the semantic token definition, not in component
CSS. A component consuming font-display-hero gets fluid behavior
without writing media queries. This pushes responsive concerns down to
the token layer where they belong.
Body and heading tokens remain fixed because reading rhythm depends on stable line lengths. Fluid body text destabilizes measure and makes layout brittle.
Consequences
Positive:
- Templates and AGENTS.md describe typography by purpose, not size.
- Fluid display sizing is automatic for any component using a display token.
- Theme variation has room to adjust scale per role.
- The "pick by size" failure mode for AI agents disappears.
- HTML tag mapping (
h1→ heading.title; landing H1 → display.hero) becomes the discoverable pattern.
Negative / tradeoffs:
- Migration touches every component CSS file that references typography (estimated ~all 10 v0.1.0 components).
- Designers fluent in t-shirt sizing must learn the role vocabulary. Mitigation: visual reference page in docs site shows side-by-side comparison with size labels.
- Some legitimate edge cases (a deliberately tiny H1 in a sidebar widget) require manual override. The component-specific tier handles this.
Alternatives considered
- Keep t-shirt naming with documentation discipline. Rejected: v0.1.0 demonstrated that role intent gets lost in size names.
- Add a parallel
display.*set, keep size names elsewhere. Rejected: two parallel naming schemes is worse than one principled rename. We accept the migration cost once. - Fluid sizing in component CSS via media queries. Rejected: pushes responsive logic up from the token layer. Tokens are the right home for fluid scaling because it is a property of the typographic role.
- Use full sentence-case names like
font-page-title-large. Rejected: verbose without adding meaning. The group prefix (display/heading/body/special) already carries role.
References
docs-internal/architecture/v1-master-diff-merge.md§3.4, §6.2.docs-internal/architecture/v1-principles.md— principles P-3.4, P-6.x.llm-wiki/decisions/0008-token-tier-strict-separation.md— semantic tier is the only public consumption surface; these tokens are part of it.