Design tokens explained for developers
What design tokens are, how the W3C DTCG JSON format works, how to name and layer them, and how to turn one token file into CSS variables and TypeScript.
By The M1rror team7 min read
If you have ever written --color-primary: #4f46e5; in a stylesheet, you have already used a design token in spirit. Design tokens are named design decisions: this is our accent colour, this is our medium spacing step, this is the radius for controls. What turns a handful of CSS variables into a proper token system is structure, a shared format and a single source of truth that designers and developers both work from. This post explains tokens from a developer's point of view: what they are, how the standard JSON format works, how to organise them and how to get from a token file to code you can ship.
What a design token is
The W3C Design Tokens Community Group's Format Module describes design tokens as "a methodology for expressing design decisions in a platform-agnostic way", and defines a token as information associated with a human-readable name, at minimum a name/value pair.
The important words are decision and platform-agnostic. A token is not a hex code. It is the decision that "muted text uses this grey", written down once and then compiled to whatever each platform needs: CSS custom properties for the web, constants for TypeScript, resources for iOS and Android.
Tokens usually cover:
- colour
- spacing and sizing
- typography (font families, sizes, weights, line heights)
- border radius and border width
- shadows and elevation
- motion (durations and easing curves)
Why not just use CSS variables?
CSS custom properties are a great output for tokens, and for a single web app they can be enough. But they have limits as a source of truth:
- They only exist in CSS. Your TypeScript, charts, emails, native apps and design tools cannot read them without a runtime.
- They carry no type information.
--space-4and--color-textlook the same to a tool. - They have no place for metadata such as descriptions, deprecation notes or which theme a value belongs to.
A token file solves these by storing decisions in structured data and generating CSS variables (and everything else) from it.
The DTCG format in practice
The Design Tokens Community Group announced its first stable specification, version 2025.10, in October 2025. The format is plain JSON, and the spec recommends the .tokens or .tokens.json file extensions.
A token is an object with a required $value. Optional properties include $type and $description. Here is a small file:
{
"color": {
"$type": "color",
"indigo-600": {
"$value": {
"colorSpace": "srgb",
"components": [0.31, 0.275, 0.898],
"hex": "#4f46e5"
},
"$description": "Brand indigo"
}
},
"space": {
"$type": "dimension",
"4": { "$value": { "value": 16, "unit": "px" } },
"6": { "$value": { "value": 24, "unit": "px" } }
}
}A few things to notice:
- Groups are objects without a
$value. Herecolorandspaceare groups. - Types can be set on a group and are inherited by the tokens inside it, so you do not repeat
$typeon every token. - Colours are objects with a colour space and numeric components, optionally with a hex fallback. Dimensions are objects with a numeric
valueand aunitofpxorrem.
The spec defines a set of basic types (colour, dimension, font family, font weight, duration, cubic Bézier and number) and composite types for things that need several values together: stroke style, border, transition, shadow, gradient and typography.
Aliases
Tokens can reference other tokens. In the DTCG format a reference is the dotted path to another token wrapped in curly braces:
{
"color": {
"$type": "color",
"indigo-600": {
"$value": { "colorSpace": "srgb", "components": [0.31, 0.275, 0.898], "hex": "#4f46e5" }
},
"accent": { "$value": "{color.indigo-600}" }
}
}color.accent now resolves to the full value of color.indigo-600. Change the indigo and the accent follows. Aliases are how you build layers, which is the most important organising idea in any token system.
Layering tokens: primitive, semantic, component
Most token systems end up with two or three tiers.
Primitive tokens
The raw palette and scales, named by what they are: gray-100 to gray-900, indigo-600, space-1 to space-16, radius-sm. Primitives are rarely used directly in components.
Semantic tokens
Aliases named by purpose: color.text, color.text-muted, color.surface, color.border, color.accent, color.danger. These are what components should use. They answer "what is this for?" rather than "what does it look like?".
Component tokens (optional)
Aliases scoped to a component: button.primary.background, input.border. They are useful in large systems where a component needs to diverge from the semantic default without affecting everything else. Small teams can usually skip this tier until they need it.
The payoff of layering is that most changes touch one tier. A rebrand changes primitives. Dark mode remaps semantic tokens to different primitives. A one-off tweak to buttons changes a component token. Component code does not change in any of these cases.
Naming tokens
Naming causes more debate than any other part of token work. A few rules that hold up:
- Name by role, not appearance, for anything components use.
text-mutedsurvives a palette change;gray-textdoes not. - Be consistent about order. Pick something like category, then role, then variant, then state (
color.accent.hover) and stick to it. - Use numeric scales for primitives.
space-4is easier to reason about thanspace-medium, and leaves room to add steps. - Avoid encoding values in names.
space-16pxbecomes a lie the moment you switch torem.
From token file to code
Tokens are only useful once they reach the codebase. The usual approach is a build step that reads the token file and writes platform outputs. The DTCG announcement names Style Dictionary, Tokens Studio and Terrazzo among the reference implementations of the format. For a small project, a short script can be enough:
// scripts/build-tokens.ts
import { readFileSync, writeFileSync } from "node:fs";
type Token = { $value: unknown; $type?: string };
type Group = { [key: string]: Token | Group | string | undefined };
const tokens = JSON.parse(readFileSync("tokens/base.tokens.json", "utf8")) as Group;
const flat = new Map<string, unknown>();
function walk(node: Group, path: string[]) {
for (const [key, child] of Object.entries(node)) {
if (key.startsWith("$") || typeof child !== "object" || child === null) continue;
if ("$value" in child) flat.set([...path, key].join("."), (child as Token).$value);
else walk(child as Group, [...path, key]);
}
}
walk(tokens, []);
function toCss(value: unknown): string {
if (typeof value === "string" && value.startsWith("{")) {
return `var(--${value.slice(1, -1).replaceAll(".", "-")})`;
}
if (value && typeof value === "object") {
const v = value as { hex?: string; value?: number; unit?: string };
if (v.hex) return v.hex;
if (v.unit) return `${v.value}${v.unit}`;
}
return String(value);
}
const lines = [...flat].map(([name, value]) => ` --${name.replaceAll(".", "-")}: ${toCss(value)};`);
writeFileSync("src/styles/tokens.css", `:root {\n${lines.join("\n")}\n}\n`);That script handles colours, dimensions and simple aliases; a real tool also handles composite types, themes and validation. The output is ordinary CSS:
:root {
--color-indigo-600: #4f46e5;
--color-accent: var(--color-indigo-600);
--space-4: 16px;
--space-6: 24px;
}Aliases become var() references, so the relationship between tokens survives into the browser. Because CSS custom properties cascade and inherit, you can override semantic tokens on a container to theme a section of the page, or inside a prefers-color-scheme media query for dark mode.
It is also worth generating a TypeScript file of token names so that code referencing tokens is checked at compile time:
export const tokens = {
color: { accent: "var(--color-accent)", textMuted: "var(--color-text-muted)" },
space: { 4: "var(--space-4)", 6: "var(--space-6)" },
} as const;Tokens and accessibility
Tokens make accessibility rules enforceable. Because every text and surface pairing goes through semantic tokens, you can check contrast once per pairing instead of once per screen. WCAG 2.2 requires at least 4.5:1 contrast for normal text, 3:1 for large text, and 3:1 for the visual information that identifies UI components and their states. Add a test that resolves each semantic text token against each surface token it is used on and fails if the ratio drops below the threshold. When someone tweaks a primitive, the test tells you which pairings broke.
Common mistakes
- Tokenising everything at once. Start with colour, spacing and type. Add motion and elevation once those are stable.
- Too many primitives. Twelve greys that nobody can tell apart are not a scale. Fewer, clearly distinct steps are easier to use correctly.
- Components using primitives directly. It works until the first theme change, then every component needs editing.
- Two sources of truth. If designers edit values in a design tool and developers edit a CSS file, they will diverge. Decide which file is canonical and generate the rest.
Where designers fit in
Tokens are a shared language between design and engineering, so they work best when both sides review changes to them. Treat a token change like an API change: propose it, show its effect on real screens, get agreement, then release it as a new version.
M1rror's System mode is built around this: you can import a DTCG or Tokens Studio JSON file, review and approve the system with your team, then export it as CSS variables, TypeScript, flat JSON, DTCG, Markdown docs, SwiftUI, Android XML or Jetpack Compose. If you do not have tokens yet, you can also capture a website you own to extract its palette, type, spacing, radii and shadows as a starting point.
Wrap-up
Design tokens are named design decisions stored in a structured, platform-agnostic format. The DTCG format gives you a stable JSON shape with types, groups and aliases. Layer your tokens into primitives and semantics, name them by role, generate CSS variables and TypeScript from a single source, and test the pairings that matter for accessibility. If your values are currently scattered across stylesheets, start by turning your existing CSS into a design system, then move the result into a token file.