Design & ProductARTICLE

Design tokens in practice: from Figma to Style Dictionary following the W3C spec

A step-by-step guide for going from loose colors to a design system with typed tokens, aliases, and light/dark themes, exported via Style Dictionary and validated against the W3C format.

Design tokens in practice: from Figma to Style Dictionary following the W3C spec
Image: Yara Uchôa

Design tokens in practice: from Figma to Style Dictionary following the W3C spec

A step-by-step guide for going from loose colors to a design system with typed tokens, aliases, and light/dark themes, exported via Style Dictionary and validated against the W3C format.

The problem always starts the same way: someone on the team opens Figma, creates a color palette with names like Blue/500, and three months later nobody knows which blue is the primary button's, which is the link's, and which one is left over from a test. The value #3366FF is copied by hand in 40 places in the CSS. When the design calls for dark mode, the honest answer is: this is going to hurt.

Design tokens exist to solve exactly this: separate the design decision ("the primary action color is this blue") from the raw value (#3366FF) and from the place where it's used (button.background). What's new in the Design Tokens Format Module, maintained by the W3C's Design Tokens Community Group, is a standardized JSON file format for exchanging these tokens between tools, instead of each tool inventing its own export.

I'll show the path I'd take to go from a file of loose colors to a design system with typed tokens, aliases, and themes, exported with Style Dictionary and with validated JSON. An important warning right away: the version of the spec I read is a preview draft (2025.10), and the document itself states, in bold letters, "do not implement this version." In other words, the conceptual structure ($value, $type, groups, aliases) is stable and worth understanding, but some color syntax details are still in flux. I'll use the classic form that tools already support today and note where the new spec diverges.

Editor's note: the quote above, "do not implement this version," is the author's paraphrase, not a literal transcription of the document. The original text reads "Do not attempt to implement this version of the specification. Do not reference this version as authoritative in any way" and, elsewhere, "do not implement anything in this document." The gist of the warning is preserved, but formatting it as a direct quote led the reader to think it was an exact transcription, which is not the case.

Prerequisites

| Tool | Reference version I used | For what | |---|---|---| | Node.js | 20 LTS | running Style Dictionary | | Style Dictionary | 4.x | transforming tokens into CSS/JS | | ajv-cli | 5.x | validating JSON against a schema |

Create the folder and install:

bash
mkdir ds-tokens && cd ds-tokens
npm init -y
npm install --save-dev style-dictionary ajv-cli
mkdir -p tokens build schema

Step 1: primitive tokens (the raw palette)

The first layer has no semantics at all. These are the raw values, the equivalent of your Blue/500 from Figma. An object becomes a token when it has the $value property, and becomes a group when it doesn't: that's the spec's central rule. $value is a reserved word.

tokens/color.base.json:

json
{
  "color": {
    "$type": "color",
    "blue": {
      "500": { "$value": "#3366ff" },
      "700": { "$value": "#1a3fcc" }
    },
    "gray": {
      "0":   { "$value": "#ffffff" },
      "900": { "$value": "#111418" }
    }
  }
}

Notice I put $type: "color" on the color group, not on each token. The spec says the type is inherited from the nearest parent group that declares $type. This saves repetition and is the first concrete gain over Figma, where each value is an island.

Groups are arbitrary and tools SHOULD NOT use them to infer the type or purpose of design tokens.

>

-- Design Tokens Format Module, W3C DTCG

In other words: grouping by blue is human organization, not semantics. What carries semantics is the $type and, in the next layer, the name.

Step 2: semantic tokens with aliases

This is where the trick lies. Instead of button.background pointing directly to #3366ff, it references the primitive token. The spec calls this an alias, and the syntax is the token's name inside curly braces, separated by dots: {color.blue.500}. That's why dots and braces are forbidden in token names.

tokens/color.semantic.json:

json
{
  "action": {
    "$type": "color",
    "primary": {
      "bg":    { "$value": "{color.blue.500}" },
      "bg-hover": { "$value": "{color.blue.700}" },
      "text":  { "$value": "{color.gray.0}" }
    }
  }
}

When the $value is a reference, the token's $type is the resolved type of the referenced token. No need to repeat it. And the practical benefit: if tomorrow the brand's blue changes, you swap #3366ff in one place (color.blue.500) and everyone who has an alias follows along.

Step 3: light and dark themes

There are several ways to model a theme. The one I'd choose to start with is the most readable: two files of semantic tokens that override the same names with different aliases, and Style Dictionary generates two CSS files.

tokens/theme.dark.json:

json
{
  "action": {
    "$type": "color",
    "primary": {
      "text": { "$value": "{color.gray.900}" }
    }
  }
}

In dark mode, the text over the primary button changes. The bg stays blue, but the text becomes the dark gray. Each theme is a different composition of aliases over the same primitive palette, and this is where it becomes clear why the layer separation isn't nitpicking: without it, dark mode would mean copying and pasting hex values.

Step 4: configuring Style Dictionary

Editor's note: the code in this section and in the ajv validation section (Step 6) was not executed in a real environment. Validate before using in production.

Style Dictionary is named explicitly by the spec as a translation tool: it takes the token JSON and spits out platform code. Since the script uses top-level import/await (ES Modules syntax), save the file as config.mjs (with the .mjs extension), so Node interprets the file as a module without needing to touch the package.json generated by npm init -y.

config.mjs:

js
import StyleDictionary from 'style-dictionary';

const base = ['tokens/color.base.json', 'tokens/color.semantic.json'];

function build(theme, files) {
  const sd = new StyleDictionary({
    source: files,
    platforms: {
      css: {
        transformGroup: 'css',
        buildPath: 'build/',
        files: [{
          destination: `tokens.${theme}.css`,
          format: 'css/variables',
          options: { selector: theme === 'light' ? ':root' : '[data-theme="dark"]' }
        }]
      }
    }
  });
  return sd.buildAllPlatforms();
}

await build('light', base);
await build('dark', [...base, 'tokens/theme.dark.json']);

Run:

bash
node config.mjs

build/tokens.light.css comes out like this:

css
:root {
  --color-blue-500: #3366ff;
  --action-primary-bg: #3366ff;
  --action-primary-text: #ffffff;
}

And tokens.dark.css brings --action-primary-text: #111418; inside [data-theme="dark"]. Note that Style Dictionary resolved the aliases: in the final CSS the alias became the concrete value. This is the expected behavior of a translation tool.

Step 5: checking that it worked

"It compiled" isn't the same as "it's correct." Two checks I wouldn't skip.

First, the dumb visual test: an HTML page with the button and a data-theme toggle. If the button's text changes color when you switch, the aliases are flowing.

html
<link rel="stylesheet" href="build/tokens.light.css">
<link rel="stylesheet" href="build/tokens.dark.css">
<button style="background: var(--action-primary-bg); color: var(--action-primary-text)">
  Enviar
</button>

Second, and this is the sensitive accessibility point: contrast is part of the definition of done. A color token that passes in light mode may fail in dark mode. Before considering a token finished, run the bg/text pair of each theme through a contrast checker (the target is 4.5:1 for normal text, WCAG AA). A token that looks nice but fails contrast is a bug, not a design decision.

Step 6: validating the JSON against a schema

The spec still has an editor's note saying the group "is exploring adding a JSON Schema" to the format, meaning there's no official schema published within the 2025.10 draft. So the honest validation here is against a schema that reflects the normative rules: every object either has $value or is a group, names don't start with $ nor contain {, }, or ., and $type is a string.

Create the minimal schema before running the validation:

bash
mkdir -p schema
cat > schema/dtcg.schema.json << 'EOF'
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "patternProperties": {
    "^(?!\\$)[^.{}]+$": {
      "type": "object"
    }
  },
  "additionalProperties": true
}
EOF

It's deliberately simple: it checks the most critical rule (names without forbidden characters) and serves as a starting point, not a complete validation of the spec. Now run ajv:

bash
npx ajv validate -s schema/dtcg.schema.json -d "tokens/*.json"

If you have a token named "my.token" with a dot in the name, or an object that has both $value and children at the same time (the spec says to treat this as an error, since something can't be both a token and a group at once), validation will flag it. It's worth automating this in a pre-commit hook: a broken token shouldn't reach the build.

The stumbles I'd expect

  • Alias that doesn't resolve. Writing {color.blue.500} but the token is named color.blue.500 inside a group with a different $type, or getting the path wrong. Style Dictionary complains about a reference not found. Check the full path from the root of the JSON.
  • Forgotten $type. Without $type on the token nor on any parent group, and without being an alias, the spec says to consider the token invalid. The tool shouldn't "guess" by looking at the value. If something doesn't export, check the type inheritance.
  • Names that only differ by case. font-size and FONT-SIZE are distinct, valid tokens per the spec, but when exporting to Sass they become the same variable and one silently overwrites the other. Avoid this.
  • Jumping into the new color syntax too soon. The spec's preview uses color objects with colorSpace and components ([1, 0, 0]). Production tools still work mostly with hex strings. Since the document itself asks not to implement this version, stick with what your toolchain supports today and follow the stable versions before migrating.

The real value of this exercise isn't the generated CSS, it's the structure: primitives that nobody references directly, semantics that name the decision, themes that recompose aliases. With this, changing the entire brand or adding a third theme stops being a hunt for hex values and becomes swapping half a dozen references, with the schema ensuring nobody wrote nonsense along the way.

Source 1: Design Tokens Format Module — W3C Design Tokens Community Group (https://tr.designtokens.org/format/)

Translated from the Brazilian Portuguese original · Read the original

View profile →