How design tokens work, from a JSON file to CSS variables
Design tokens explained with a real JSON example: naming, aliases, a dark theme, and a Node script that turns a token file into CSS variables.
A design token is a named design decision: a name, a value, and optionally a type and a description. color-text-primary: #000000 and font-size-heading-level-1: 44px are both tokens. You store them in one file, and tools turn that file into whatever each platform needs, such as CSS variables, Sass variables, or native code.
The practical workflow is short. Write tokens in the W3C Design Tokens Community Group JSON format, give the raw values plain names, add semantic aliases on top, and run a script that emits CSS custom properties. The script further down does exactly that, including a dark theme, using nothing but Node.js.
What design tokens mean in practice#
The Design Tokens Format Module describes design tokens as a methodology for expressing design decisions in a platform-agnostic way, so they can be shared across disciplines, tools, and technologies. They also give a team a common vocabulary.
The reason to bother is the glue code. Many tools can export tokens or expose an API for them, but each does it in its own way. A team ends up maintaining its own conversion scripts, and switching tools means rewriting them. A shared file format is meant to lower that cost, and it's the main reason the community group's spec exists.
That's also the answer to "why design tokens" for most teams: one source of truth, with design and code kept in sync from it. If you only have one product and one developer, a handful of CSS variables may be all you need. Tokens start paying off when more than one tool or platform reads the same values.
The token file format (W3C draft spec)#
The spec is a draft Community Group Report, not a finished W3C Recommendation, so expect details to move. The version I'm describing is the 2025.10 module. Every token file is valid JSON.
Here's the smallest useful file:
{
"$schema": "https://www.designtokens.org/schemas/2025.10/format.json",
"color": {
"$type": "color",
"blue": {
"500": {
"$value": {
"colorSpace": "srgb",
"components": [0.145, 0.388, 0.922],
"alpha": 1,
"hex": "#2563eb"
},
"$description": "Primary brand blue"
}
}
}
}
The color value shape (colorSpace, components, alpha, hex) matches the shadow example in the spec. Names starting with $ are reserved properties:
| Property | What it does |
|---|---|
$value |
The token's value. A token is an object that has one. |
$type |
Declares what kind of value is allowed. Can sit on the token or on a parent group. |
$description |
Optional human-readable note. |
$extensions |
Optional object for tool-specific data, keyed with a vendor-specific key (reverse domain notation is recommended). |
$root |
Reserved token name for a group's base value alongside its children. |
$extends |
Mentioned in the group resolution order for inheriting tokens. |
Two rules matter when you write files by hand. First, a token needs a type: from its own $type, from a parent group, or from the token it aliases. If none is set, tools must treat the token as invalid and must not guess from the value. Second, if the value doesn't match the declared type, that's an error too.
For file names, the spec recommends the extensions .tokens and .tokens.json, and the media type application/design-tokens+json. Plain application/json is also allowed. I'd use .tokens.json for now, since editors open it as JSON without extra setup.
Design token naming examples#
The spec says groups are arbitrary and tools shouldn't infer a token's type or purpose from the group it sits in. So the structure is yours to design. Translation tools typically build a flat name by joining the token's path, because paths are unique within a file even when plain names aren't. The spec's own example is a tool like Style Dictionary concatenating a path into Sass variable names.
That gives a simple mapping:
| Token path | CSS variable |
|---|---|
color.blue.500 |
--color-blue-500 |
space.md |
--space-md |
color.surface |
--color-surface |
A few naming habits I'd recommend (this is advice, not spec):
- Put the category first (
color,space) so related variables sort together. - Name raw values by what they are (
blue-500), and name semantic tokens by what they're for (surface,text,accent). - Don't bake a theme into a name.
color-text-darkbreaks the day you add a third theme. - Keep names lowercase and hyphen-friendly, since they'll become CSS identifiers.
Aliases and semantic design tokens#
An alias is a token whose value is a reference to another token. The syntax is curly braces around the dotted path:
"accent": { "$value": "{color.blue.500}" }
The spec also supports JSON Pointer references with $ref, but it points out that curly-brace references are design-token-specific: they resolve $value automatically and can only target tokens. For everyday aliasing, braces are what you want.
A two-layer setup is the common pattern. The first layer is raw values (color.blue.500). The second layer is semantic tokens (color.accent, color.surface) that alias into the first. Components only ever use the second layer. When the brand blue changes, you edit one value. When you add a theme, you repoint aliases instead of touching components.
One limit to remember: groups can't be referenced, only tokens can. Composite tokens, like a shadow made of color, offsets, blur, and spread, are tokens, so they can be.
Themes: light and dark from the same names#
The pack I'm working from doesn't define a theming mechanism, so what follows is a convention rather than something from the spec. The idea is simple: keep the raw layer shared, and make one file per theme that defines the same semantic names with different aliases.
Light maps color.surface to white and color.text to a near-black gray. Dark maps the same two names to the opposite grays. In CSS, the light theme lands in :root and the dark overrides land under a selector such as [data-theme="dark"]. Because the semantic variables are emitted as var(--color-gray-900) rather than a copied hex value, the raw layer stays the single place a color is defined.
A script that turns a token file into CSS variables#
This runs in plain Node.js with no packages. The token data is inline so the example is self-contained, but it has the same shape as a .tokens.json file. It handles group-level $type, $root, curly-brace aliases, and two types (color and dimension), and it fails loudly on anything else.
const base = JSON.parse(`{
"color": {
"$type": "color",
"white": { "$value": { "colorSpace": "srgb", "components": [1, 1, 1], "alpha": 1, "hex": "#ffffff" } },
"gray": {
"50": { "$value": { "colorSpace": "srgb", "components": [0.976, 0.98, 0.984], "alpha": 1, "hex": "#f9fafb" } },
"900": { "$value": { "colorSpace": "srgb", "components": [0.067, 0.094, 0.153], "alpha": 1, "hex": "#111827" } }
},
"blue": {
"500": { "$value": { "colorSpace": "srgb", "components": [0.145, 0.388, 0.922], "alpha": 1, "hex": "#2563eb" } }
},
"surface": { "$value": "{color.white}" },
"text": { "$value": "{color.gray.900}" },
"accent": { "$value": "{color.blue.500}" }
},
"space": {
"$type": "dimension",
"sm": { "$value": { "value": 0.5, "unit": "rem" } },
"md": { "$value": { "value": 1, "unit": "rem" } }
}
}`);
const dark = JSON.parse(`{
"color": {
"$type": "color",
"surface": { "$value": "{color.gray.900}" },
"text": { "$value": "{color.gray.50}" }
}
}`);
const toName = (path) => "--" + path.join("-");
// Walk groups, inherit $type, and collect tokens keyed by dotted path.
function flatten(node, path = [], inherited, out = new Map()) {
const type = node.$type ?? inherited;
for (const [key, child] of Object.entries(node)) {
if (key.startsWith("$") && key !== "$root") continue;
const p = key === "$root" ? path : [...path, key];
if ("$value" in child) {
out.set(p.join("."), { path: p, value: child.$value, type: child.$type ?? type });
} else {
flatten(child, p, type, out);
}
}
return out;
}
function toCss(token, known) {
const { value, type } = token;
if (typeof value === "string" && /^\{.+\}$/.test(value)) {
const ref = value.slice(1, -1);
if (!known.has(ref)) throw new Error(`Unknown reference ${value} in ${token.path.join(".")}`);
return `var(${toName(ref.split("."))})`;
}
if (!type) throw new Error(`No type for ${token.path.join(".")}`);
if (type === "color") return value.hex;
if (type === "dimension") return `${value.value}${value.unit}`;
throw new Error(`Unsupported type "${type}" for ${token.path.join(".")}`);
}
function block(selector, tokens, known) {
const lines = [...tokens.values()].map((t) => ` ${toName(t.path)}: ${toCss(t, known)};`);
return `${selector} {\n${lines.join("\n")}\n}`;
}
const baseTokens = flatten(base);
const darkTokens = flatten(dark);
const known = new Map([...baseTokens, ...darkTokens]);
console.log(block(":root", baseTokens, known));
console.log(block('[data-theme="dark"]', darkTokens, known));
// A broken alias should fail, not silently produce bad CSS.
try {
const bad = flatten({ color: { $type: "color", oops: { $value: "{color.nope}" } } });
toCss(bad.get("color.oops"), bad);
} catch (err) {
console.log("Caught:", err.message);
}
A few things to try. Change color.blue.500 and watch --color-accent follow, since it's emitted as a var() reference. Add a "$root" token inside the blue group to see it take the name --color-blue. Add a token with an unsupported type and confirm the script refuses it.
On the CSS side, the output is ordinary custom properties, so components just use background: var(--color-surface).
Design tokens in Figma and other tools#
The spec lists Figma alongside Penpot and Sketch as UI design tools, and it describes the intended flow: designers label and reference shared values in the design tool, and translation tools convert exported token data into platform code. The source material I'm using doesn't cover Figma's own menus or variable features, so for step-by-step instructions on creating tokens inside Figma, check Figma's documentation for your plan and version.
What I can say is how to stay portable. Keep your canonical file in the standard format, and treat whatever a design tool exports as an input you convert into it. Tool-specific extras belong in $extensions under a vendor key, and the spec says tools must preserve extension data they don't understand.
Common mistakes and gotchas#
- No type anywhere. A token without its own
$type, an inherited group$type, or a typed alias target is invalid. Put$typeon the group when everything in it shares a type. - Inferring meaning from groups. Don't build logic that assumes anything in a group called
coloris a color. The spec says tools must not infer type or purpose from groups. Declare$type. - Referencing a group.
{color.blue}points at a group, not a token. References target tokens only. - Copying hex values into the theme layer. If dark mode repeats
#111827instead of aliasing, you now have two places to update. - Themed names. Names like
button-bg-darklock a theme into the API. Name by role and swap the alias. - Dropping unknown extension data. If you write your own tool that rewrites token files, preserve
$extensionsyou don't understand. - Treating the draft as frozen. The spec is a Community Group draft. Pin the schema version you wrote against, as in the
$schemaline above. - Silent failures in your build. As in the script, throw on unknown references and unsupported types instead of emitting partial CSS.
Quick reference checklist#
- Store tokens in a
.tokens.jsonfile using$value,$type, and optional$description. - Set
$typeon groups to avoid repeating it. - Name raw tokens by what they are, and semantic tokens by what they do.
- Alias with
{group.token}, and alias in the semantic layer only. - Join the path with hyphens to get CSS variable names.
- Emit semantic tokens as
var(--...)so themes override only the aliases. - Fail the build on unresolved references or unsupported types.
- Keep vendor data in
$extensions.
The spec's source lives in the design-tokens/community-group repository, where you can follow open issues and changes.
Quick answers
What is the W3C design tokens standard?
It's a draft format from the Design Tokens Community Group for exchanging design tokens between tools as JSON. It defines properties like $value, $type, and $description, plus references and groups. It's a Community Group Report rather than a finished W3C Recommendation.
How do I create design tokens?
Create a .tokens.json file, group related tokens, and give each token a $value and a $type (directly or via its group). Add semantic aliases like {color.blue.500} on top of raw values, then convert the file to CSS variables with a script or a translation tool.
What's the difference between a group and a composite token?
A group only organizes tokens for naming and can't be referenced. A composite token, such as a shadow, is a single token whose value has several named sub-values, so other tokens can reference it.
Can I create design tokens in Figma?
The spec lists Figma as a design tool where designers can label and reference shared values. The source material here doesn't cover Figma's specific steps, so follow Figma's own documentation for that part.
Next note