UI/UX Atlas
Typography Intermediate

Typography Design Tokens

Structured, platform-agnostic typographic values that bridge design decisions and code — keeping every surface consistent without manual drift.

7 min read

The full lesson

Keeping typography consistent across a large product is nearly impossible to do by hand. When a codebase spans multiple platforms, teams, and themes, a font-size value written in one place quietly drifts away from the same value written elsewhere six months later.

Design tokens solve that problem. They give every typographic value a single home. Typography is especially token-dense — one body text style alone needs at least five values: family, size, weight, line-height, and letter-spacing. Tokens keep all of those in sync automatically.

This lesson covers how to structure typographic tokens, how the W3C DTCG format works, and how to wire tokens into CSS, Figma, and your component library so all three stay in sync.

Why Typography Needs Its Own Token Layer

Color tokens get most of the attention in design-system writing, but typography needs tokens just as much. A single body text style involves at least five values: family, size, weight, line-height, and letter-spacing. Multiply that across every text role — display, heading 1–4, body, caption, overline, code — and every theme variant, and you have hundreds of values that change together or break together.

Here is what you get by tokenizing those values:

  • Single source of truth. Change a base size in one file; every platform that consumes the token updates automatically.
  • Auditable design decisions. Tokens make implicit choices explicit. A reviewer can diff a token file and immediately see what changed and why.
  • Theming without re-implementation. Dark mode, high-contrast mode, brand variants, and white-label products become theme files that swap token values — not code rewrites.
  • Handoff that stays accurate. When Figma token values come from the same source that feeds the CSS build, the spec and the implementation cannot diverge.

The Three-Tier Token Architecture

Good token systems separate concerns into three layers. Collapsing everything into one flat list is the most common mistake teams make.

Tier 1 — Primitive (raw) tokens

Primitives are raw, named constants. They carry no semantic meaning — they simply give stable names to values so you can reference them elsewhere.

{
  "font-size-1": { "$value": "0.75rem", "$type": "dimension" },
  "font-size-2": { "$value": "0.875rem", "$type": "dimension" },
  "font-size-3": { "$value": "1rem", "$type": "dimension" },
  "font-size-4": { "$value": "1.125rem", "$type": "dimension" },
  "font-size-5": { "$value": "1.25rem", "$type": "dimension" },
  "font-size-6": { "$value": "1.5rem", "$type": "dimension" },
  "font-size-7": { "$value": "2rem", "$type": "dimension" },
  "font-size-8": { "$value": "3rem", "$type": "dimension" },

  "font-weight-regular": { "$value": 400, "$type": "fontWeight" },
  "font-weight-medium": { "$value": 500, "$type": "fontWeight" },
  "font-weight-semibold": { "$value": 600, "$type": "fontWeight" },
  "font-weight-bold": { "$value": 700, "$type": "fontWeight" },

  "line-height-tight": { "$value": 1.2, "$type": "number" },
  "line-height-snug": { "$value": 1.35, "$type": "number" },
  "line-height-normal": { "$value": 1.5, "$type": "number" },
  "line-height-relaxed": { "$value": 1.65, "$type": "number" },

  "font-family-sans": { "$value": "'Inter Variable', system-ui, sans-serif", "$type": "fontFamily" },
  "font-family-mono": { "$value": "'JetBrains Mono', 'Fira Code', monospace", "$type": "fontFamily" }
}

Use numeric names here (font-size-1 through font-size-8) rather than semantic names. The primitive tier is a lookup table, not a contract.

Tier 2 — Semantic tokens

Semantic tokens map roles to primitives. They answer “what is this value used for?” rather than “what is this value?”

{
  "typography-body-font-size": {
    "$value": "{font-size-3}",
    "$type": "dimension"
  },
  "typography-body-line-height": {
    "$value": "{line-height-normal}",
    "$type": "number"
  },
  "typography-body-font-weight": {
    "$value": "{font-weight-regular}",
    "$type": "fontWeight"
  },
  "typography-heading-1-font-size": {
    "$value": "{font-size-8}",
    "$type": "dimension"
  },
  "typography-heading-1-line-height": {
    "$value": "{line-height-tight}",
    "$type": "number"
  },
  "typography-caption-font-size": {
    "$value": "{font-size-1}",
    "$type": "dimension"
  },
  "typography-code-font-family": {
    "$value": "{font-family-mono}",
    "$type": "fontFamily"
  }
}

Themes swap semantic token values without touching primitives or components. A high-contrast theme might point typography-body-font-size to a larger primitive. A compact-density variant points it to a smaller one.

Tier 3 — Component tokens

Component tokens scope values to a single component. They reference semantic tokens — or occasionally primitives directly when a semantic token doesn’t exist yet.

{
  "button-label-font-size": {
    "$value": "{typography-body-font-size}",
    "$type": "dimension"
  },
  "button-label-font-weight": {
    "$value": "{font-weight-semibold}",
    "$type": "fontWeight"
  },
  "tooltip-font-size": {
    "$value": "{typography-caption-font-size}",
    "$type": "dimension"
  }
}

Component tokens let you tune a single component without accidentally changing anything else. They also enable per-brand or per-tenant theming at the component level. A white-label partner can override button-label-font-weight without touching the global semantic layer.

DTCG JSON Format in Practice

The W3C DTCG format uses a small set of reserved keys, each prefixed with $:

KeyPurpose
$valueThe token’s resolved value (required)
$typeData type: dimension, fontWeight, fontFamily, number, string, duration, etc.
$descriptionHuman-readable rationale — treated as documentation
$extensionsArbitrary tool-specific metadata (Figma IDs, Tokens Studio overrides, etc.)

Tokens can be nested in groups using plain object keys (no $ prefix). A well-organised file looks like this:

{
  "font": {
    "size": {
      "body": {
        "$value": "1rem",
        "$type": "dimension",
        "$description": "Default body text. Resolves to 16px at browser default."
      },
      "sm": {
        "$value": "0.875rem",
        "$type": "dimension"
      }
    },
    "weight": {
      "body": { "$value": 400, "$type": "fontWeight" },
      "emphasis": { "$value": 600, "$type": "fontWeight" }
    }
  }
}

Style Dictionary resolves reference chains like {font.size.body} at build time and outputs CSS custom properties, JavaScript modules, Swift/Kotlin constants, or any other format your platform needs.

Generating CSS Custom Properties

Style Dictionary is the most widely adopted token-to-code build tool. Given the JSON source above, a build config outputs CSS like this:

:root {
  --font-size-body: 1rem;
  --font-size-sm: 0.875rem;
  --font-weight-body: 400;
  --font-weight-emphasis: 600;
  --font-family-sans: 'Inter Variable', system-ui, sans-serif;
}

In your component CSS, reference the custom property instead of the raw value:

.body-text {
  font-size: var(--font-size-body);
  font-weight: var(--font-weight-body);
  line-height: var(--line-height-normal);
  font-family: var(--font-family-sans);
}

Theming is then as simple as scoping overrides to a data attribute or class:

[data-theme="compact"] {
  --font-size-body: 0.875rem;
  --line-height-normal: 1.4;
}

No component code changes. No rebuild required for theme switches at runtime.

Fluid Typography and Tokens Together

Fluid type sizes — generated with clamp() — should be tokenised too. The primitive token holds the clamp expression, keeping the fluid calculation in one place:

{
  "font-size-fluid-body": {
    "$value": "clamp(0.9rem, 0.85rem + 0.25vw, 1.05rem)",
    "$type": "dimension",
    "$description": "Fluid body text: scales between ~14.4px and ~16.8px across viewport widths."
  },
  "font-size-fluid-h1": {
    "$value": "clamp(2rem, 1.5rem + 2.5vw, 4rem)",
    "$type": "dimension"
  }
}

This is much safer than sprinkling clamp() expressions across component CSS files, where the floor and ceiling values inevitably drift. Centralise the formula in the token. Let every consumer reference it.

Typography Tokens in Figma

Tokens Studio (formerly Figma Tokens) and Figma Variables both support typed token collections. The recommended workflow:

  1. Source of truth in code. Maintain the canonical DTCG JSON in your repository.
  2. Sync to Figma via Tokens Studio’s GitHub/GitLab sync, or a CI step that pushes variable updates via the Figma REST API.
  3. Designers consume tokens, not raw values. In Figma, text styles are bound to token values rather than hardcoded numbers. Changing a token in the JSON file propagates to Figma on the next sync — not on the next designer update.

Figma Variables (introduced in 2023) now support number and string types directly, making font-size and font-weight mappable as variables. Font-family tokens still require the Figma text styles layer for now — Figma Variables do not yet bind directly to font families in the same way.

Do

Name semantic tokens by their role and scale: typography-heading-2-font-size, typography-label-font-weight. Keep primitive names abstract and ordinal: font-size-5, font-weight-semibold. Sync Figma from the same JSON source that feeds your CSS build.

Don't

Name tokens after raw values (font-size-16, bold) or after primitive colours (blue-text). Maintain separate token files for web, iOS, and Android — they should all be generated from the same source JSON. Never hardcode font values directly in component CSS when a token exists.

Dark Mode and Theming

A three-tier token system makes theme switching straightforward. The semantic tier defines the role. The theme overrides the value for that role.

{
  "typography-body-font-size": { "$value": "{font-size-3}" }
}

A compact-density theme file overrides only the semantic values it needs to change:

{
  "typography-body-font-size": { "$value": "{font-size-2}" },
  "typography-heading-1-font-size": { "$value": "{font-size-7}" }
}

The primitive scale and all component tokens stay unchanged. Style Dictionary merges the base and theme files and outputs a scoped CSS block for [data-density="compact"].

For dark mode specifically: typographic tokens rarely need to change between light and dark themes. Font-size, weight, and line-height are usually stable — only colour tokens shift. The one exception is letter-spacing. Some teams slightly increase letter-spacing on light-coloured text on dark backgrounds, because perceived contrast is slightly lower at identical spacing. That is a subtle but legitimate token difference worth encoding.

Token Naming Conventions and Governance

A naming convention only works if you enforce it. The recommended structure is:

[category]-[variant]-[property]

Examples:

  • typography-body-font-size
  • typography-heading-1-line-height
  • typography-label-font-weight
  • typography-code-font-family

Governance rules that prevent entropy:

  • No raw values in component CSS. All text properties must reference a CSS custom property backed by a token. Enforce this with a stylelint rule or a token-lint CI step.
  • Semantic tokens only in components. Components must not reference primitive tokens directly (e.g., var(--font-size-3)) — they must go through the semantic layer so theming works.
  • Deprecation over deletion. When a token is retired, mark it with "$extensions": { "deprecated": true } before removing it. This gives consuming teams a migration window.
  • Changelog required for token changes. Token renames or value changes are breaking changes for any platform that consumes the output. Treat them with the same rigour as API changes.

Connecting Tokens to Storybook and Living Docs

A token system that only lives in a JSON file is under-documented. The loop closes when:

  1. Storybook reads the CSS custom properties from the same build artifact the production app uses. Token values are therefore automatically visible in rendered stories.
  2. A token documentation page (generated by Style Dictionary’s docs transform or a custom Storybook add-on) shows every typographic token with its resolved value, its aliases, and its description.
  3. Figma Dev Mode or Code Connect maps design components directly to Storybook stories, so developers see the actual token names used in implementation rather than flat values from a redline.

This closes the design-to-code gap without a separate spec document — the spec is the running implementation.