EduFlow

Section 2 · Design Tokens

Token Pipelines: From JSON to Every Platform

20 min3 min readFill in the blanks

Lesson at a glance

Set up a token build pipeline that turns DTCG JSON into CSS custom properties per theme and native formats per platform, keeping references intact so runtime theming still works.

  1. 1What a pipeline does
  2. 2Keep the references
  3. 3A real pipeline with Style Dictionary
  4. 4Checks that run with the build

No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.

With tokens stored as DTCG JSON, something has to turn them into what each platform consumes: CSS custom properties for the web app and marketing site, Swift for the iOS driver app, Kotlin or XML for Android. That something is a token pipeline. It’s a small piece of code that every design system team ends up owning, and getting it right early saves months of drift later.

What a pipeline does

Token JSON files flow through parse, resolve, transform and format steps, producing CSS for web, Swift for iOS and Kotlin for Android. *.tokens .json Parse Resolve Transform Format CSS Swift Kotlin
A token pipeline: one source, one output per platform
  1. Parse every source file and deep-merge them into one tree.
  2. Resolve aliases like {color.blue.600}, rejecting broken or circular references.
  3. Transform names and values for each platform: color.action.bg becomes --nw-color-action-bg in CSS and colorActionBg in Swift; 1rem becomes 16 points on iOS.
  4. Format the result as a file: a block of custom properties, a Swift enum, an Android resource file.

Here’s the heart of the CSS side, small enough to read in one go:

JavaScript
const tokens = {
  color: {
    $type: 'color',
    blue: { 600: { $value: { colorSpace: 'srgb', components: [0.122, 0.353, 0.878], hex: '#1f5ae0' } } },
    white: { $value: { colorSpace: 'srgb', components: [1, 1, 1], hex: '#ffffff' } },
    action: {
      bg: { $value: '{color.blue.600}' },
      text: { $value: '{color.white}' },
    },
  },
  space: { $type: 'dimension', 4: { $value: { value: 1, unit: 'rem' } } },
};

// Walk the tree and collect every token with its path.
function flatten(node, path = [], type) {
  const t = node.$type ?? type;
  if ('$value' in node) return [{ path, type: t, value: node.$value }];
  return Object.entries(node)
    .filter(([key]) => !key.startsWith('
    
  

))
    .flatMap(([key, child]) => flatten(child, [...path, key], t));
}

const cssName = (path) => `--nw-${path.join('-')}`;
function cssValue({ type, value }) {
  if (typeof value === 'string' && value.startsWith('{')) {
    return `var(${cssName(value.slice(1, -1).split('.'))})`; // keep the reference
  }
  if (type === 'color') return value.hex;
  if (type === 'dimension') return `${value.value}${value.unit}`;
  return String(value);
}

const lines = flatten(tokens).map((t) => `  ${cssName(t.path)}: ${cssValue(t)};`);
console.log(`/* Generated from tokens. Do not edit. */\n:root {\n${lines.join('\n')}\n}`);

Look at the output. The path becomes the name, so color.blue.600 comes out as --nw-color-blue-600; earlier lessons shortened primitives to --nw-blue-600 for readability, but in a generated system the name is whatever the group structure says, which is one more reason to design groups and names together. More importantly, semantic tokens come out as var(--nw-color-blue-600), not as a hex value. That choice is important enough to deserve its own section.

Keep the references

A pipeline can either resolve aliases to final values or keep them as references in the output. For CSS, keep them. Resolved output flattens your tiers: the browser sees --nw-color-action-bg: #1f5ae0, so overriding a primitive at runtime does nothing, and a developer inspecting a button in DevTools can’t see which primitive it comes from. Referenced output preserves the exact structure you designed, including the subtree theming from the themes lesson.

Native platforms are different. Swift and Kotlin constants are usually resolved to final values, because those platforms handle themes through their own mechanisms (asset catalogs, Compose color schemes).

A real pipeline with Style Dictionary

You could grow the script above into a full tool, but most teams use Style Dictionary, the most widely used open-source token build system. Recent versions read DTCG files natively and auto-detect the format. A Northwind-style config builds the light theme into :root and the dark theme under its attribute selector:

build-tokens.mjs
import StyleDictionary from 'style-dictionary';

const themes = {
  light: ':root',
  dark: '[data-theme="dark"]',
};

for (const [name, selector] of Object.entries(themes)) {
  const sd = new StyleDictionary({
    include: ['tokens/primitives.tokens.json'],
    source: [`tokens/semantic-${name}.tokens.json`],
    platforms: {
      css: {
        transformGroup: 'css',
        prefix: 'nw',
        buildPath: 'dist/css/',
        files: [{
          destination: `theme-${name}.css`,
          format: 'css/variables',
          filter: name === 'light' ? undefined : (token) => token.isSource,
          options: { selector, outputReferences: true },
        }],
      },
    },
  });
  await sd.buildAllPlatforms();
}

The key ideas: primitives come in through include, so references can resolve, while the theme’s semantic file comes in through source. For dark, the filter keeps only source tokens, so the dark file contains the remapped semantic tokens and not a second copy of every primitive. outputReferences: true keeps var() references in the output. Style Dictionary may warn that the dark file references tokens it filtered out; here that is intended, because the light file already defines the primitives on :root.

Checks that run with the build

Once tokens are data, CI can test design decisions like code. Northwind’s pipeline runs three checks on every pull request: the naming lint from earlier, a reference check that fails on broken or circular aliases, and a contrast check that resolves each documented text and background pair in every theme and fails below 4.5:1. That last one caught a dark-mode regression in the Tidewater brand before any designer saw it.

In the exercise, you complete the dark theme’s build config. That finishes the token architecture; the next section builds components on top of it.

Key takeaways

  • A token pipeline parses source files, resolves aliases, transforms names and values per platform, and writes one output format per platform.
  • Keep references in CSS output (outputReferences) so semantic tokens still point at primitives at runtime and subtree theming keeps working.
  • Build each theme from shared primitives plus that theme’s semantic file, and emit only the theme’s own tokens under its selector.
  • Generated files are build output; nobody edits them by hand, and CI rebuilds them on every change.

Further reading

Check your understanding

3 questions

Question 1 of 3

The pipeline writes --nw-color-action-bg: #1f5ae0; instead of --nw-color-action-bg: var(--nw-color-blue-600);. What breaks?

Question 2 of 3

Why does the dark theme build use include for primitives and source for the dark semantic file, then filter on isSource?

Question 3 of 3

A product engineer edits dist/css/theme-dark.css directly to fix a contrast bug, and the fix vanishes after the next release. What should the process have been?

Practice

Fill in the blanks

Complete the dark theme build

Fill in the Style Dictionary config that generates Northwind’s dark theme. The output must:

  • name every variable with the nw prefix (--nw-…),
  • use Style Dictionary’s built-in CSS custom properties format,
  • contain only the dark semantic tokens, not the primitives loaded through include,
  • wrap them in the attribute selector that switches any element to dark, written with double quotes inside the single-quoted string,
  • keep var() references instead of resolved values.
export default { include: ['tokens/primitives.tokens.json'], source: ['tokens/semantic-dark.tokens.json'], platforms: { css: { transformGroup: 'css', prefix: '', buildPath: 'dist/css/', files: [{ destination: 'theme-dark.css', format: '', filter: (token) => token., options: { selector: '', outputReferences: , }, }], }, }, };