opsinjs
FoundationsToken familiesTypography

Typography tokens

The generated reference for every type role, with a rendered specimen for each. Each entry lists the role's size, line height, tracking and weight.

Overview

Every type role in the system, with the four values it resolves to and the components that consume it. Generated from tokens/type.json by scripts/build-tokens.mts; nothing here is typed by a person, and pnpm check:generated fails the build if it is.

The specimens render from the generated values rather than from a screenshot, so what you see is the token itself. They also render at more than one text-size multiplier, because a type token whose value you know at 1× and not at 1.3× is a token you do not actually know.

How it works

tokens/type.json holds one entry per role, and each entry is four values:

  • size is expressed relative to the root, never in pixels, so the reader's multiplier and browser zoom both apply.
  • line height is a unitless ratio, so it scales with the size rather than being recomputed per role.
  • tracking is a letter-spacing adjustment, negative at the large end and slightly positive at the small end. Type set at one tracking across a whole scale looks wrong at both extremes.
  • weight is a numeric weight the platform font can actually produce.

There is no per-role tabular flag and no scaleRate field, because the file holds neither. Tabular figures are not a property of a size role at all. The product theme sets font-variant-numeric: tabular-nums on anything carrying the data-opsinjs-value attribute, and the numeric family is reached through font-opsin-numeric, so a measurement gets aligned digits from what it is rather than from the step it sits in. Every role also scales by the same factor, because the reader's multiplier is applied once at the root font size and never tuned per role. See Type scale.

The generator emits both a --opsin-text-<role>-* group of custom properties and a Tailwind --text-* mapping, so a role is reachable as a utility class or as a raw variable depending on how the consuming product is built.

Using it

Read the Used by column first. It is the fastest way to find the role you want: if a role is used by nothing, either you have found the one you need or the role should not exist.

Set a number in a value context, not just in a size role. A size role gives a number its size; it is the data-opsinjs-value attribute and font-opsin-numeric that give it tabular, aligned digits. A number rendered without them shifts the layout every time it changes, whichever role it is in.

Copy the role name, never the computed size. A pixel value pasted into a component is a component that no longer participates in Dynamic Type, and the failure only appears for readers who have changed their text size. Those are the readers least likely to be in the room when it is reviewed.

Compare across text sizes, not just at 1×. The specimens on this page are rendered at several multipliers precisely so that a role which looks correct by default and wrong at 1.3× is visible here rather than in production.

Tokens

TokenWhat it controlsUsed by
--opsin-font-sanssystem-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serifThe sans family stack.no component
--opsin-font-monoui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospaceThe mono family stack.no component
--opsin-font-numericsystem-ui, -apple-system, 'Segoe UI', Roboto, sans-serifThe numeric family stack.result-card, value
--opsin-text-large-title-size2.125remThe largest step in the scale, reserved for a surface whose entire subject is one number or one word. No component in the system sets a value at this step yet.no component
--opsin-text-large-title-leading1.206Line height for largeTitle, unitless so it scales with the size.no component
--opsin-text-large-title-tracking-0.011emLetter spacing for largeTitle.no component
--opsin-text-large-title-weight700Font weight for largeTitle.no component
--opsin-text-title1-size1.75remA screen title, and the hero treatment a value's number takes at size display.result-card, value
--opsin-text-title1-leading1.214Line height for title1, unitless so it scales with the size.result-card, value
--opsin-text-title1-tracking-0.009emLetter spacing for title1.result-card, value
--opsin-text-title1-weight700Font weight for title1.result-card, value
--opsin-text-title2-size1.375remSection title; the floor for a primary health value inside a card.goal-ring
--opsin-text-title2-leading1.273Line height for title2, unitless so it scales with the size.goal-ring
--opsin-text-title2-tracking-0.006emLetter spacing for title2.goal-ring
--opsin-text-title2-weight600Font weight for title2.goal-ring
--opsin-text-title3-size1.25remA Dialog or Sheet title, and the unit beside a value's number at size display.dialog, questionnaire, reading-input, sheet, value
--opsin-text-title3-leading1.25Line height for title3, unitless so it scales with the size.dialog, questionnaire, reading-input, sheet, value
--opsin-text-title3-tracking-0.004emLetter spacing for title3.dialog, questionnaire, reading-input, sheet, value
--opsin-text-title3-weight600Font weight for title3.dialog, questionnaire, reading-input, sheet, value
--opsin-text-headline-size1.0625remAn emphasised line of body text, such as a status sentence or a question in a form.accordion, alert-banner, avatar, button, callout, card, care-card, empty-state, field, log-sheet, menu, popover, range-bar, reading-input, result-card, scale-input, segmented-control, status-pill, surface, tabs, textarea, timeline-entry, toast, trend-sparkline, value
--opsin-text-headline-leading1.294Line height for headline, unitless so it scales with the size.accordion, alert-banner, avatar, button, callout, card, care-card, empty-state, field, log-sheet, menu, popover, range-bar, reading-input, result-card, scale-input, segmented-control, status-pill, surface, tabs, textarea, timeline-entry, toast, trend-sparkline, value
--opsin-text-headline-tracking-0.003emLetter spacing for headline.accordion, alert-banner, avatar, button, callout, card, care-card, empty-state, field, log-sheet, menu, popover, range-bar, reading-input, result-card, scale-input, segmented-control, status-pill, surface, tabs, textarea, timeline-entry, toast, trend-sparkline, value
--opsin-text-headline-weight600Font weight for headline.accordion, alert-banner, avatar, button, callout, card, care-card, empty-state, field, log-sheet, menu, popover, range-bar, reading-input, result-card, scale-input, segmented-control, status-pill, surface, tabs, textarea, timeline-entry, toast, trend-sparkline, value
--opsin-text-body-size1.0625remEverything a reader reads as prose. The anchor of the scale.accordion, alert-banner, badge, callout, card, care-card, checkbox, combobox, consent-sheet, dialog, disclaimer-note, divider, dose-tracker, empty-state, field, link, menu, number-field, popover, questionnaire, radio-group, range-bar, range-legend, reading-input, relative-time, result-card, score-dial, scroll-area, select, sheet, surface, switch, symptom-picker, table, tabs, term, textarea, timeline-entry, toast, tooltip, trend-sparkline, value, visually-hidden
--opsin-text-body-leading1.294Line height for body, unitless so it scales with the size.accordion, alert-banner, badge, callout, card, care-card, checkbox, combobox, consent-sheet, dialog, disclaimer-note, divider, dose-tracker, empty-state, field, link, menu, number-field, popover, questionnaire, radio-group, range-bar, range-legend, reading-input, relative-time, result-card, score-dial, scroll-area, select, sheet, skeleton, surface, switch, symptom-picker, table, tabs, term, textarea, timeline-entry, toast, tooltip, trend-sparkline, value, visually-hidden
--opsin-text-body-tracking-0.003emLetter spacing for body.accordion, alert-banner, badge, callout, card, care-card, checkbox, combobox, consent-sheet, dialog, disclaimer-note, divider, dose-tracker, empty-state, field, link, menu, number-field, popover, questionnaire, radio-group, range-bar, range-legend, reading-input, relative-time, result-card, score-dial, scroll-area, select, sheet, surface, switch, symptom-picker, table, tabs, term, textarea, timeline-entry, toast, tooltip, trend-sparkline, value, visually-hidden
--opsin-text-body-weight400Font weight for body.accordion, alert-banner, badge, callout, card, care-card, checkbox, combobox, consent-sheet, dialog, disclaimer-note, divider, dose-tracker, empty-state, field, link, menu, number-field, popover, questionnaire, radio-group, range-bar, range-legend, reading-input, relative-time, result-card, score-dial, scroll-area, select, sheet, surface, switch, symptom-picker, table, tabs, term, textarea, timeline-entry, toast, tooltip, trend-sparkline, value, visually-hidden
--opsin-text-callout-size1remSecondary prose inside a card or a callout.dialog, sheet, toast
--opsin-text-callout-leading1.313Line height for callout, unitless so it scales with the size.dialog, sheet, toast
--opsin-text-callout-tracking-0.002emLetter spacing for callout.dialog, sheet, toast
--opsin-text-callout-weight400Font weight for callout.dialog, sheet, toast
--opsin-text-subheadline-size0.9375remA supporting line under a title.button, care-card, empty-state, goal-ring, metric-tile, progress, radio-group, score-dial, segmented-control, slider, spinner, status-pill, stepper, table
--opsin-text-subheadline-leading1.333Line height for subheadline, unitless so it scales with the size.button, care-card, empty-state, goal-ring, metric-tile, progress, radio-group, score-dial, segmented-control, slider, spinner, status-pill, stepper, table
--opsin-text-subheadline-tracking-0.001emLetter spacing for subheadline.button, care-card, empty-state, goal-ring, metric-tile, progress, radio-group, score-dial, segmented-control, slider, spinner, status-pill, stepper, table
--opsin-text-subheadline-weight400Font weight for subheadline.button, care-card, empty-state, goal-ring, metric-tile, progress, radio-group, score-dial, segmented-control, slider, spinner, status-pill, stepper, table
--opsin-text-subheadline-emphasis-size0.9375remThe emphasised twin of subheadline, for a compact control's label, where the control must read as narrower and never as quieter.avatar, button
--opsin-text-subheadline-emphasis-leading1.333Line height for subheadlineEmphasis, unitless so it scales with the size.avatar, button
--opsin-text-subheadline-emphasis-tracking-0.001emLetter spacing for subheadlineEmphasis.avatar, button
--opsin-text-subheadline-emphasis-weight600Font weight for subheadlineEmphasis.avatar, button
--opsin-text-footnote-size0.8125remProvenance: who measured this, when, with what.body-map, card, checkbox, combobox, consent-sheet, dialog, disclaimer-note, divider, dose-tracker, goal-ring, log-sheet, menu, metric-tile, questionnaire, radio-group, range-bar, range-legend, result-card, scale-input, score-dial, sheet, source-citation, status-pill, stepper, switch, symptom-picker, table, timeline-entry, toast, tooltip, trend-sparkline
--opsin-text-footnote-leading1.385Line height for footnote, unitless so it scales with the size.body-map, card, checkbox, combobox, consent-sheet, dialog, disclaimer-note, divider, dose-tracker, goal-ring, log-sheet, menu, metric-tile, questionnaire, radio-group, range-bar, range-legend, result-card, scale-input, score-dial, sheet, source-citation, status-pill, stepper, switch, symptom-picker, table, timeline-entry, toast, tooltip, trend-sparkline
--opsin-text-footnote-tracking0emLetter spacing for footnote.body-map, card, checkbox, combobox, consent-sheet, dialog, disclaimer-note, divider, dose-tracker, goal-ring, log-sheet, menu, metric-tile, questionnaire, radio-group, range-bar, range-legend, result-card, scale-input, score-dial, sheet, source-citation, status-pill, stepper, switch, symptom-picker, table, timeline-entry, toast, tooltip, trend-sparkline
--opsin-text-footnote-weight400Font weight for footnote.body-map, card, checkbox, combobox, consent-sheet, dialog, disclaimer-note, divider, dose-tracker, goal-ring, log-sheet, menu, metric-tile, questionnaire, radio-group, range-bar, range-legend, result-card, scale-input, score-dial, sheet, source-citation, status-pill, stepper, switch, symptom-picker, table, timeline-entry, toast, tooltip, trend-sparkline
--opsin-text-caption1-size0.75remAxis labels, legend text, and legal text.avatar, badge, consent-sheet, dose-tracker, range-bar, skeleton, tab-bar, visually-hidden
--opsin-text-caption1-leading1.333Line height for caption1, unitless so it scales with the size.avatar, badge, consent-sheet, dose-tracker, range-bar, skeleton, tab-bar, visually-hidden
--opsin-text-caption1-tracking0.002emLetter spacing for caption1.avatar, badge, consent-sheet, dose-tracker, range-bar, skeleton, tab-bar, visually-hidden
--opsin-text-caption1-weight400Font weight for caption1.avatar, badge, consent-sheet, dose-tracker, range-bar, skeleton, tab-bar, visually-hidden
--opsin-text-caption2-size0.6875remThe smallest text the system permits, and only for text that repeats a label already present elsewhere. Never the only place a fact appears.no component
--opsin-text-caption2-leading1.182Line height for caption2, unitless so it scales with the size.no component
--opsin-text-caption2-tracking0.005emLetter spacing for caption2.no component
--opsin-text-caption2-weight500Font weight for caption2.no component
--opsin-numeralstabular-numsEvery component that renders a number sets this, so a changing value does not shift its own layout.no component

Your blood pressure was 148 over 92

At 200%

Your blood pressure was 148 over 92

Accessibility impact

  • The generated table is what makes the scale reviewable. Because every value is emitted rather than typed, "the type scales" stops being a claim: anyone can read the size and weight of every role, at more than one multiplier, and check it against what renders.
  • Every pairing of a role with a surface is measured for contrast at the role's own size and weight. APCA is size- and weight-sensitive, so a single ratio for the whole scale would be wrong for most of it. The measured results are on Contrast conformance.
  • The minimum size is published and enforced. caption2 is the floor; a generated table makes it obvious when something has been set below it.
  • The specimens are real text. They are selectable, translatable and readable by a screen reader, not images of type. That is also the rule the rest of the system follows.
  • Type scale says what each role is for, and covers how a measurement gets tabular figures.
  • Dynamic Type says what these values become at the reader's own text size, and covers the 1.3× bar.
  • Reference → Tokens has the same rows alongside every other family.

On this page