--- title: "Tokens" subtitle: "The primitive layer everything else resolves through" description: "The design system's primitive tokens: the unit scale, the type scale, line heights, weights and tracking, and where every other token family is documented." author: "Studio" section: "Website" subsection: "Foundations" order: 3 status: "published" --- Tokens come in two layers. Primitives are raw values with no opinion about where they are used — `--l` is 16px, `--font-m` is 18px, and neither knows what it is for. Semantic tokens name an intent and resolve through a primitive, so `--text-primary` is the colour body text takes, whatever that colour turns out to be. Page and component code uses the semantic layer; this page documents the primitive layer beneath it. ## Why the split exists Reaching past a semantic token to a primitive is the mistake this architecture is built to prevent. `--neutral-800` is a grey; `--text-secondary` is *the colour supporting text takes*, which today happens to be that grey. Write the primitive and the element stops following the theme — it will not invert in dark mode, and a palette change leaves it behind. - **Use a semantic token in page and component code.** If no semantic token fits, the semantic layer is missing one, and the fix is to add it rather than to reach past it. - **Use a primitive only when defining a semantic token.** That is the one place the raw value belongs. ## The unit scale Every spacing and sizing token resolves through this scale, so the whole system moves together if it is re-stepped. Values are rem, which means they track the reader's browser zoom and root font-size preference rather than pinning to device pixels. | Token | Value | Token | Value | | --- | --- | --- | --- | | `var(--none)` | 0 | `var(--5xl)` | 3.5rem · 56px | | `var(--2xs)` | 0.125rem · 2px | `var(--6xl)` | 4rem · 64px | | `var(--xs)` | 0.25rem · 4px | `var(--7xl)` | 4.5rem · 72px | | `var(--s)` | 0.5rem · 8px | `var(--8xl)` | 5rem · 80px | | `var(--m)` | 0.75rem · 12px | `var(--9xl)` | 5.5rem · 88px | | `var(--l)` | 1rem · 16px | `var(--10xl)` | 6rem · 96px | | `var(--xl)` | 1.5rem · 24px | `var(--11xl)` | 6.5rem · 104px | | `var(--2xl)` | 2rem · 32px | `var(--12xl)` | 7rem · 112px | | `var(--3xl)` | 2.5rem · 40px | `var(--13xl)` | 7.5rem · 120px | | `var(--4xl)` | 3rem · 48px | `var(--14xl)` | 10rem · 160px | The `--space-*` tokens are aliases onto this scale — `--space-l` is `var(--l)` — and they are what layout code actually uses. The [Spacing](/website/spacing.html) page covers them and the utilities that apply them. ## Type scale Font sizes are primitives. Nothing here says which element takes which step; that mapping is the [web hierarchy](/website/typography.html) on the Typography page. | Token | Value | Token | Value | | --- | --- | --- | --- | | `var(--font-3xs)` | 0.625rem · 10px | `var(--font-3xl)` | 1.75rem · 28px | | `var(--font-2xs)` | 0.75rem · 12px | `var(--font-4xl)` | 2rem · 32px | | `var(--font-xs)` | 0.875rem · 14px | `var(--font-5xl)` | 2.25rem · 36px | | `var(--font-s)` | 1rem · 16px | `var(--font-6xl)` | 2.5rem · 40px | | `var(--font-m)` | 1.125rem · 18px | `var(--font-7xl)` | 3rem · 48px | | `var(--font-l)` | 1.25rem · 20px | `var(--font-8xl)` | 3.5rem · 56px | | `var(--font-xl)` | 1.375rem · 22px | `var(--font-9xl)` | 4rem · 64px | | `var(--font-2xl)` | 1.5rem · 24px | `var(--font-10xl)` | 4.5rem · 72px |

--font-10xl · 72px

Where Culture Meets Care

--font-9xl · 64px

Where Culture Meets Care

--font-8xl · 56px

Where Culture Meets Care

--font-7xl · 48px

Where Culture Meets Care

--font-6xl · 40px

Where Culture Meets Care

--font-5xl · 36px

Where Culture Meets Care

--font-4xl · 32px

Where Culture Meets Care

--font-3xl · 28px

Where Culture Meets Care

--font-2xl · 24px

Where Culture Meets Care

--font-xl · 22px

Where Culture Meets Care

--font-l · 20px

Where Culture Meets Care

--font-m · 18px

Where Culture Meets Care

--font-s · 16px

Where Culture Meets Care

--font-xs · 14px

Where Culture Meets Care

--font-2xs · 12px

Where Culture Meets Care

--font-3xs · 10px

Where Culture Meets Care

## Line height | Token | Value | Use | | --- | --- | --- | | `var(--line-height-xs)` | 0.7 | Tight display settings where the leading would otherwise open the line up | | `var(--line-height-s)` | 1 | Single-line controls — buttons, badges, anything that never wraps | | `var(--line-height-m)` | 1.2 | Headings, where the type is large enough that generous leading reads as a gap | | `var(--line-height-l)` | 1.4 | Sub-headings and short passages | | `var(--line-height-xl)` | 1.6 | Body copy, which is the one place leading does the most work for readability | | `var(--line-height-2xl)` | 1.8 | A long measure that needs more air than body copy | ## Weight The tokens span 300 to 900, but the loaded faces do not. **Erode is a variable font covering 300–700; Inter is loaded at 400–700.** So 400 to 700 is the range both faces render natively, 300 is available in Erode only, and nothing on the site provides 800 or 900 — a browser asked for those will clamp to 700 or synthesise a heavier face. | Token | Value | Token | Value | | --- | --- | --- | --- | | `var(--font-weight-light)` | 300 | `var(--font-weight-semi-bold)` | 600 | | `var(--font-weight-regular)` | 400 | `var(--font-weight-bold)` | 700 | | `var(--font-weight-book)` | 450 | `var(--font-weight-extra-bold)` | 800 | | `var(--font-weight-medium)` | 500 | `var(--font-weight-black)` | 900 | - **Stay between 400 and 700.** Those are the weights both faces actually ship; 800 and 900 are declared tokens with no loaded face behind them. ## Letter spacing | Token | Value | Use | | --- | --- | --- | | `var(--letter-spacing-xs)` | -0.03em | Large headings, where default tracking looks loose | | `var(--letter-spacing-s)` | 0.03em | A slight opening, for small text set in caps | | `var(--letter-spacing-m)` | 0.06em | Labels | | `var(--letter-spacing-l)` | 0.12em | Eyebrows and badges — the widest setting in regular use | | `var(--letter-spacing-xl)` | 0.24em | Reserved for display settings at large sizes | ## Alpha The alpha tokens are `color-mix` operands, not colours. `--alpha-10` expands to `transparent 90%`, so `color-mix(in srgb, var(--text-primary), var(--alpha-10))` is the text colour at 10 % strength. They step in fives from `--alpha-5` to `--alpha-95`. ```css background: color-mix(in srgb, var(--text-primary), var(--alpha-10)); ``` This is how the faded surfaces and hairlines are built, and it is why they follow the theme: mixing against a semantic token inverts when that token does. ## Where the rest is documented No family is listed twice, so each one has a single page that owns its values. | Family | Documented on | | --- | --- | | Colour — semantic tokens, light and dark | [Colour](/website/color.html) | | Colour — the neutral and gold primitive ramps | [Brand Book → Colour](/brand/color.html) | | Font slots, and which element takes which size | [Typography](/website/typography.html) | | `--space-*` and the spacing utilities | [Spacing](/website/spacing.html) | | `--target-min`, the 44px floor for anything pressable | [Spacing](/website/spacing.html) | | The four `--safe-area-*` insets | [Spacing](/website/spacing.html) | | Border widths and radius | [Border](/website/border.html) | | Containers and max-widths | [Layout](/website/layout.html) | | Easing and duration | [Motion](/website/motion.html) | | Component tokens | Each component's own page, as those pages land | ## Declared but unread Some role tokens ship in the engine — four of them re-stepped at the breakpoints — but no rule reads them: `--headline-size`, `--headline-weight`, `--headline-leading`, `--headline-tracking`, `--headline-width`, `--subline-size`, `--subline-weight`, `--title-size`, `--label-weight`, `--label-leading` and `--label-tracking`. - **Do not build against them.** Setting one changes nothing today, and a display class has to set `font-family` itself because `--title-family` is the only role family token that exists. This is recorded as an engine gap in `docs/figma-mapping.md`. The fix belongs upstream — either ship the classes that read them, or drop the tokens.