
How to Set Up Typography Tokens in a Design System?
- Sajjad
- Typography
- 01 Oct, 2026
Your product has a marketing site in Next.js, a dashboard in React, an iOS app, and a set of Figma libraries. Somebody bumps the body size from 16px to 17px in Figma. Three months later, the web app is on 16px, the marketing site is on 17px, iOS is on 17pt with a different line height, and nobody can say which is correct. This is the problem design tokens solve: one source of truth for typographic decisions, stored as data, transformed automatically into whatever each platform needs.
This article explains what typography tokens are, how to structure them in tiers, how to write them in the W3C Design Tokens format, how to build them into CSS custom properties and a Tailwind v4 theme, and how to handle responsive sizes and naming without creating a token sprawl nobody can navigate.
What Are Typography Tokens?
A design token is a named design decision stored in a platform-neutral format, usually JSON. A typography token captures one aspect of type: a font family, a size, a weight, a line height, a letter spacing, or a composite of all of them.
Instead of hardcoding font-size: 1.125rem in a component, you reference a token like --font-size-body-lg. When the decision changes, you change the token, rebuild, and every platform picks it up.
Typography tokens typically cover:
| Token type | Example name | Example value |
|---|---|---|
| Font family | font.family.sans | Inter, system-ui, sans-serif |
| Font size | font.size.400 | 1rem |
| Font weight | font.weight.semibold | 600 |
| Line height | font.line-height.normal | 1.5 |
| Letter spacing | font.letter-spacing.tight | -0.01em |
| Composite style | typography.heading.lg | family + size + weight + leading + tracking |
Tokens are not a replacement for CSS. They are the layer above it, the source your CSS, Swift, Kotlin, and Figma variables are generated from.
Structuring Tokens in Three Tiers
Most mature design systems organize tokens into three tiers. Each tier references the one below it.
1. Primitive Tokens
Primitive (or core, or global) tokens are the raw palette: every font size in your scale, every weight you load, every line height you allow. They have neutral names that describe the value's position, not its use.
{
"font": {
"family": {
"sans": { "$type": "fontFamily", "$value": ["Inter", "system-ui", "sans-serif"] },
"serif": { "$type": "fontFamily", "$value": ["Source Serif 4", "Georgia", "serif"] },
"mono": { "$type": "fontFamily", "$value": ["JetBrains Mono", "ui-monospace", "monospace"] }
},
"size": {
"200": { "$type": "dimension", "$value": { "value": 0.8125, "unit": "rem" } },
"300": { "$type": "dimension", "$value": { "value": 0.875, "unit": "rem" } },
"400": { "$type": "dimension", "$value": { "value": 1, "unit": "rem" } },
"500": { "$type": "dimension", "$value": { "value": 1.25, "unit": "rem" } },
"600": { "$type": "dimension", "$value": { "value": 1.5625, "unit": "rem" } },
"700": { "$type": "dimension", "$value": { "value": 1.953, "unit": "rem" } },
"800": { "$type": "dimension", "$value": { "value": 2.441, "unit": "rem" } }
},
"weight": {
"regular": { "$type": "fontWeight", "$value": 400 },
"medium": { "$type": "fontWeight", "$value": 500 },
"semibold": { "$type": "fontWeight", "$value": 600 },
"bold": { "$type": "fontWeight", "$value": 700 }
},
"line-height": {
"tight": { "$type": "number", "$value": 1.15 },
"snug": { "$type": "number", "$value": 1.3 },
"normal": { "$type": "number", "$value": 1.5 },
"relaxed": { "$type": "number", "$value": 1.65 }
},
"letter-spacing": {
"tight": { "$type": "dimension", "$value": { "value": -0.02, "unit": "em" } },
"normal": { "$type": "dimension", "$value": { "value": 0, "unit": "em" } },
"wide": { "$type": "dimension", "$value": { "value": 0.06, "unit": "em" } }
}
}
}
The numeric size names (200 through 800) leave room to insert steps later without renaming everything. The sizes here follow a 1.25 ratio from a 1rem base; the method is explained in how to build a modular type scale.
2. Semantic Tokens
Semantic tokens describe purpose. They reference primitives, so the semantic layer is where design decisions actually live:
{
"typography": {
"body": {
"md": {
"$type": "typography",
"$value": {
"fontFamily": "{font.family.sans}",
"fontSize": "{font.size.400}",
"fontWeight": "{font.weight.regular}",
"lineHeight": "{font.line-height.relaxed}",
"letterSpacing": "{font.letter-spacing.normal}"
}
}
},
"heading": {
"lg": {
"$type": "typography",
"$value": {
"fontFamily": "{font.family.serif}",
"fontSize": "{font.size.700}",
"fontWeight": "{font.weight.semibold}",
"lineHeight": "{font.line-height.tight}",
"letterSpacing": "{font.letter-spacing.tight}"
}
}
},
"label": {
"sm": {
"$type": "typography",
"$value": {
"fontFamily": "{font.family.sans}",
"fontSize": "{font.size.300}",
"fontWeight": "{font.weight.medium}",
"lineHeight": "{font.line-height.snug}",
"letterSpacing": "{font.letter-spacing.wide}"
}
}
}
}
}
The curly-brace strings are aliases: references to other tokens by path. If you change font.family.serif, every heading style updates.
3. Component Tokens
Component tokens bind a semantic style to a specific component part, such as button.label.typography or card.title.typography. They are optional. Add them only when a component genuinely needs to diverge, or when you want a team to be able to restyle one component without touching the semantic layer. Many systems never need this tier for typography.
Writing Tokens in the W3C DTCG Format
The examples above use the format from the W3C Design Tokens Community Group (DTCG), which published its first stable version of the specification in 2025. The conventions are simple:
$valueholds the token's value.$typedeclares what kind of value it is:fontFamily,dimension,fontWeight,number, or the compositetypography.$descriptionis optional documentation that tools can show in Figma or a docs site.- Groups are plain nested objects without a
$value. A$typeset on a group applies to every token inside it. - Aliases use a dotted path in curly braces.
In the stable spec, dimension values are objects with a numeric value and a unit of px or rem. Earlier drafts used plain strings like "1rem", and some tools still expect that form. Check what your toolchain version supports before committing to one shape across hundreds of tokens.
A few typing rules that save debugging time:
- Line height is a plain number in the composite typography type, which maps naturally to unitless CSS line heights.
- Letter spacing is a dimension. Use
emin your CSS output even if the token is authored differently, because tracking should scale with font size. - Font weight is a number from 1 to 1000 or a named alias like
bold. Numbers are less ambiguous.
Building Tokens into CSS with Style Dictionary
Style Dictionary is the most widely used open-source token build tool. Recent versions read DTCG $value and $type syntax natively. Install it and add a config:
npm install -D style-dictionary
// style-dictionary.config.mjs
export default {
source: ["tokens/**/*.json"],
platforms: {
css: {
transformGroup: "css",
buildPath: "src/styles/tokens/",
files: [
{
destination: "typography.css",
format: "css/variables",
filter: (token) => token.path[0] === "font",
options: { outputReferences: true },
},
],
},
},
};
npx style-dictionary build --config style-dictionary.config.mjs
The output is a set of custom properties on :root:
:root {
--font-family-sans: Inter, system-ui, sans-serif;
--font-family-serif: "Source Serif 4", Georgia, serif;
--font-size-400: 1rem;
--font-size-700: 1.953rem;
--font-weight-regular: 400;
--font-weight-semibold: 600;
--font-line-height-tight: 1.15;
--font-line-height-relaxed: 1.65;
--font-letter-spacing-tight: -0.02em;
}
For composite semantic tokens, generate utility classes rather than one giant variable. A small custom format keeps the output readable:
// style-dictionary.config.mjs (excerpt)
import StyleDictionary from "style-dictionary";
StyleDictionary.registerFormat({
name: "css/typography-classes",
format: ({ dictionary }) =>
dictionary.allTokens
.filter((t) => t.$type === "typography")
.map((t) => {
const v = t.original.$value;
const ref = (alias) =>
`var(--${alias.replace(/[{}]/g, "").replaceAll(".", "-")})`;
return `.type-${t.path.slice(1).join("-")} {
font-family: ${ref(v.fontFamily)};
font-size: ${ref(v.fontSize)};
font-weight: ${ref(v.fontWeight)};
line-height: ${ref(v.lineHeight)};
letter-spacing: ${ref(v.letterSpacing)};
}`;
})
.join("\n\n"),
});
That produces classes like .type-heading-lg and .type-body-md, each pointing at primitive variables. Using variables instead of resolved values means a theme can override a primitive at runtime. For more on structuring the CSS side, see CSS custom properties for a typography system.
Feeding Tokens into Tailwind CSS v4
Tailwind v4 reads its theme from CSS variables in an @theme block, so tokens map onto it cleanly. Have Style Dictionary output a file with Tailwind's namespaces:
/* src/styles/tokens/tailwind-theme.css (generated) */
@theme {
--font-sans: Inter, system-ui, sans-serif;
--font-serif: "Source Serif 4", Georgia, serif;
--text-body-md: 1rem;
--text-body-md--line-height: 1.65;
--text-heading-lg: 1.953rem;
--text-heading-lg--line-height: 1.15;
--text-heading-lg--letter-spacing: -0.02em;
--text-heading-lg--font-weight: 600;
}
Import it after Tailwind in your main stylesheet and text-heading-lg becomes a real utility that sets size, leading, tracking, and weight together. The namespaces and options are covered in how to configure typography in Tailwind CSS v4. Mark the file as generated and keep it out of code review, since the JSON is the source of truth.
Handling Responsive and Fluid Sizes
Typography often changes with screen size. There are two clean ways to express that in tokens.
Option 1: fluid values. Store a clamp() expression as the output for display sizes. Some teams author min and max as separate primitives and let a custom transform generate the clamp:
:root {
--font-size-display: clamp(2.25rem, 1.6rem + 2.8vw, 3.5rem);
}
Option 2: modes or breakpoints. Store a small and large value and output them under a media query:
:root {
--type-heading-lg-size: 1.75rem;
}
@media (min-width: 64rem) {
:root {
--type-heading-lg-size: 2.441rem;
}
}
Fluid values produce less CSS and smoother scaling. Breakpoint modes map better to Figma, where variable modes are discrete. Either way, keep a rem term in the formula so text responds to user font settings and meets WCAG 1.4.4 (resize to 200% without loss of content).
Naming Rules That Keep Tokens Usable
Token sets fail more often from bad naming than bad values. A few rules:
- Primitives describe the value; semantics describe the role.
font.size.400andtypography.heading.lgare both good.font.size.headingis a primitive pretending to be semantic. - Do not name by pixel value.
font.size.16becomes a lie the moment the base changes. - Use t-shirt sizes or numbers consistently within a group. Mixing
sm,md,400, andlargein one group confuses everyone. - Cap the semantic set. Most products need six to ten text styles: two or three headings, two body sizes, a label, a caption, and a code style. If you have 40, designers are inventing styles per screen.
- Write
$descriptionfor anything non-obvious, such as when a label style is meant only for form fields.
Keeping Figma and Code in Sync
Figma variables now support typography-related values: strings for font families, numbers for sizes, weights, and line heights. Tools such as Tokens Studio, or Figma's REST API with a sync script, can export those variables to DTCG JSON and push them to the repository through a pull request. The goal is one direction of truth, usually from the token repository outward, with designers proposing changes through the same review process as code. The designer-to-developer side of this is covered in handing off typography from Figma to developers.
Typography Tokens FAQ
Primitive tokens hold raw values such as every size in the scale or every font weight, and are named by position. Semantic tokens describe purpose, such as body text or a large heading, and reference primitives. Components should use semantic tokens.
Use the W3C Design Tokens Community Group format, which uses dollar-prefixed value and type keys. It has a stable specification and is supported by Style Dictionary, Tokens Studio, and a growing number of design tools.
Author them in rem for web output, or keep a numeric value and let each platform transform it. Rem respects the user's browser font size setting, which matters for accessibility and WCAG resize requirements.
Most products need a primitive scale of seven to ten sizes, three or four weights, and three or four line heights, plus six to ten semantic text styles. Far more than that usually means styles are being invented per screen.
Yes. Tailwind v4 reads its theme from CSS variables in an @theme block, so a token build tool can output a file using Tailwind's namespaces, such as font and text variables, and import it into the main stylesheet.
Either store fluid clamp values for sizes that should scale smoothly, or store separate small and large values and output them under media queries. Fluid values are compact, while breakpoint modes map more directly to Figma variable modes.
Conclusion
Typography tokens turn type decisions into data that every platform can consume. Structure them in tiers, with primitives for the raw scale and semantic tokens for roles like body and heading, and add component tokens only when a component truly needs its own rules. Write them in the W3C DTCG format so your tooling and design tools speak the same language.
From there, a build step with Style Dictionary turns the JSON into CSS variables, utility classes, or a Tailwind v4 @theme file, and the same source can feed native apps and Figma. Keep the semantic set small, name tokens by role rather than value, use rem and unitless line heights, and treat generated files as build output. That is what keeps a 16px versus 17px disagreement from ever happening again.
Here are some useful references for going deeper on typography tokens:
- W3C Design Tokens Community Group: designtokens.org — home of the Design Tokens Format Module specification for
$value,$type, aliases, and composite types. - Style Dictionary: Documentation — configuration, transforms, formats, and DTCG support.
- Tailwind CSS Docs: Theme variables — the namespaces a token build can target.
- MDN Web Docs: Using CSS custom properties — how generated variables cascade and resolve.


