Skip to content

Typography

NRI typography is a role-based system of eight named styles: display, headline, title, subheading, overline, body, body-sm, and caption. Each role is expressed as --type-*-size and --type-*-line custom properties plus a class in Client/src/design-system/tokens/typography.css.

Components apply the role class or consume its variables when a class cannot reach the text, such as a pseudo-element or injected markup. They must never declare their own font-size. The one sanctioned exception is the full-screen mobile navigation, whose local variables are still composed from the shared role tokens and axes.

Display through subheading scale fluidly between viewport anchors of 390px and 1440px. Overline, body, body-sm, and caption stay fixed. Body is the baseline every text element resets to, so it has no class of its own — flow elements simply render in the body role until another role claims them.

role class size line-height weight tracking
display .display clamp(2.5rem, 1.943rem + 2.286vw, 4rem) 1.05 600 -0.02em
headline .headline clamp(1.75rem, 1.471rem + 1.143vw, 2.5rem) 1.1 600 -0.02em
title .title clamp(1.25rem, 1.157rem + 0.381vw, 1.5rem) 1.2 600 -0.01em
subheading .subheading clamp(1.0625rem, 0.993rem + 0.286vw, 1.25rem) 1.4 400 0
overline .overline 0.75rem 1 600 0.1em
body element reset (no class) 1rem 1.55 400 0
body-sm .body-sm 0.875rem 1.45 400 0
caption .caption 0.75rem 1.35 500 0.01em

The shared weight axis is deliberately small: 400 for body text, 500 for captions and light emphasis, and 600 for headings and overlines. Weight 700 is reserved for inline <strong> text.

The tracking axis gives only the roles that need optical adjustment a non-zero value: -0.02em for display and headline, -0.01em for title, 0.1em for overline, and 0.01em for caption.

Design CSS targets semantic role classes, never element tags. Heading levels h1h6 exist only to describe the document outline for accessibility and SEO; by themselves they are visually inert. Choose the correct heading element first, then apply the visual role independently.

The Umbraco TipTap editor reflects that separation with two menus:

menu purpose choices
Heading Document structure Heading 1–6 (h1h6) and Paragraph (p)
Style Visual role Overline, Headline, and Subheading

Each Style item previews its own role styling in the dropdown. Clearing a style is the toolbar’s Clear Formatting action, not a menu item. The legacy aliases are gone: .strapline was deleted 2026-08-05 and .standfirst on 2026-08-06 — .subheading is the role’s only name, in CSS and in content.

An unclassed heading rendering at body size is correct behavior. Do not add tag selectors to make a heading level look like a particular role.

--font-family-display resolves to the Newsreader serif family. Components consume this semantic token only — never --font-family-newsreader directly — so the display voice remains a system-level decision. Statement panels are the first consumer.

The display family is separate from the display size role: a component may use the display voice with another role’s size and weight when its living spec calls for that treatment.

Typography owns the space between adjacent text roles and flow content:

sequence space before the second item
overline → display, headline, or title 0.5rem
display or headline → subheading or p 1.5rem
subheading → p, ul, or ol 0.875rem
flow content → flow content 0.875rem
lili 0.5rem
flow content → display, headline, title, subheading, or overline 2rem section break

Here, flow content means address, blockquote, dd, dt, figure, figcaption, ol, p, pre, and ul. The final rule creates a deliberate section break before any heading-role class, regardless of which element carries it.

One exception: blockquote never receives the 0.875rem flow gap. The quote voice owns its own margin-block: 2rem, and neighbouring flow margins collapse against it — so a quote always sits inside 2rem of air on both sides.

Blockquotes speak in the serif pull voice: Newsreader at the subheading role, an oversized opening quote mark hanging in a 2.25rem inset, and no box chrome. The mark takes its colour from the --rich-text-link chain (theme accent on themed containers, egyptian-blue by default) while the quote text inherits, so dark panels need no extra rules. Quotes carry their own margin-block: 2rem, the weight of a section break; adjacent flow margins collapse against it.

Long-form reading width is a dimension token rather than a type role, but it governs how body lines land: --measure-prose in tokens/dimensions.css, set to 62ch. Consumers are .humble-container.-copy and the rich-text docked table-of-contents layout.

piece where responsibility
Client type system src/NRI.CMS/Client/src/design-system/tokens/typography.css Canonical role sizes, lines, classes, shared axes, aliases, and vertical rhythm
Living spec docs/design/type-roles.html Human-readable specification; update with every type-system change
Backoffice stylesheet src/NRI.CMS/wwwroot/css/rte.css Generated — typography tokens plus Client/src/styles/rte-editor.css additions, rebuilt by Client/scripts/generate-rte-css.mjs on every npm run build / npm run release; never edit by hand
RTE menus src/NRI.CMS/App_Plugins/TipTapStyles/umbraco-package.json Keeps semantic heading choice separate from visual style choice

The rule: choose a role, apply its class or consume its variables, and update both the token source and living spec when the system itself changes. The --type-* role tokens are the only source of type sizing — component-local type sizes are not part of the system.