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.
The eight roles
Section titled “The eight roles”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.
Roles, not element tags
Section titled “Roles, not element tags”Design CSS targets semantic role classes, never element tags. Heading levels h1–h6 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 (h1–h6) 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.
Display voice
Section titled “Display voice”--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.
Vertical rhythm
Section titled “Vertical rhythm”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 |
li → li |
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.
Blockquote
Section titled “Blockquote”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.
Prose measure
Section titled “Prose measure”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.
Implementation map
Section titled “Implementation map”| 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.