Malevich

Architecture decision record

ADR-0009 · Typography naming

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:

  1. 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-l blurs the design intent.
  2. AI agents over-pick the largest available size. When asked for "the main headline," an agent reading font-heading-l / font-heading-m will choose -l by default. The actual decision should hinge on the semantic role (hero vs. page title), not the visual size.
  3. 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.
  4. 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-2xl or 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

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:

Negative / tradeoffs:

Alternatives considered

References