The pain this chapter solves
Your design tokens live in a JavaScript object. Your design tool expects a JSON file in W3C format. Your build pipeline uses Style Dictionary. None of them speak the same language, so you maintain three separate representations of the same tokens.
Chapter 14
Design Token Export (DTCG)
The handoff problem
Design tokens are meant to be the single source of truth — one definition, consumed everywhere. In practice, you end up with:
- A Figma file with manually-managed color variables
- A TypeScript file with your design system tokens
- A
tokens.jsonfor Style Dictionary - A separate
variables.jsonfor Token Studio
Each one drifts. When the primary blue changes, you update three files and hope you got them all.
generateDtcgTokens() gives you a single export that speaks the W3C Design Token Community Group (DTCG) format — the emerging standard that Style Dictionary 4, Token Studio, Theo, and Specify all understand.
Basic usage
import { generateTheme, generateDtcgTokens } from 'salt-theme-gen';
const theme = generateTheme({ preset: 'ocean' });
const { light, dark, json } = generateDtcgTokens(theme);
// light — DTCG token tree for light mode (JavaScript object)
// dark — DTCG token tree for dark mode (JavaScript object)
// json — JSON string containing both: { light: {...}, dark: {...} }
Write json to a file and point Token Studio or Style Dictionary at it.
The DTCG format
Every token in the output follows the W3C format:
{
"$value": "#0077dc",
"$type": "color"
}
Dimension tokens use px units:
{
"$value": "12px",
"$type": "dimension"
}
Tokens are organized into groups — nested objects where every leaf is a token and every non-leaf is a group:
{
"color": {
"primary": { "$value": "#0077dc", "$type": "color" },
"on-primary": { "$value": "#ffffff", "$type": "color" },
"palette": {
"primary": {
"500": { "$value": "#3a7ec8", "$type": "color" }
}
}
},
"spacing": {
"md": { "$value": "12px", "$type": "dimension" }
}
}
Token hierarchy
Semantic colors — color.*
All 23 semantic color tokens, with camelCase keys converted to kebab-case:
{
"color": {
"primary": { "$value": "#0077dc", "$type": "color" },
"secondary": { "$value": "#00789a", "$type": "color" },
"background": { "$value": "#f5f8ff", "$type": "color" },
"surface": { "$value": "#ffffff", "$type": "color" },
"on-primary": { "$value": "#ffffff", "$type": "color" },
"on-secondary": { "$value": "#ffffff", "$type": "color" }
}
}
Tonal palettes — color.palette.*.*
All 88 palette steps:
{
"color": {
"palette": {
"primary": {
"50": { "$value": "#f0f6ff", "$type": "color" },
"500": { "$value": "#3a7ec8", "$type": "color" },
"950": { "$value": "#060f28", "$type": "color" }
},
"danger": {
"100": { "$value": "#ffe0e0", "$type": "color" },
"700": { "$value": "#b91c1c", "$type": "color" }
}
}
}
}
Surface elevation — color.elevation.*
The 4 surface elevation levels. Note: they live under color.elevation, not color.surface, because color.surface is already the semantic surface token.
{
"color": {
"elevation": {
"card": { "$value": "#ffffff", "$type": "color" },
"elevated":{ "$value": "#fafcff", "$type": "color" },
"modal": { "$value": "#f8fbff", "$type": "color" },
"popover": { "$value": "#f5f9ff", "$type": "color" }
}
}
}
State colors — color.state.*.*
All 32 state tokens (8 intents × 4 states):
{
"color": {
"state": {
"primary": {
"hover": { "$value": "#0068c4", "$type": "color" },
"pressed": { "$value": "#0059ac", "$type": "color" },
"focused": { "$value": "#5aacff", "$type": "color" },
"disabled": { "$value": "#a0bcd8", "$type": "color" }
}
}
}
}
Spacing — spacing.*
{
"spacing": {
"none": { "$value": "0px", "$type": "dimension" },
"xs": { "$value": "4px", "$type": "dimension" },
"sm": { "$value": "8px", "$type": "dimension" },
"md": { "$value": "12px", "$type": "dimension" },
"lg": { "$value": "16px", "$type": "dimension" },
"xl": { "$value": "24px", "$type": "dimension" },
"xxl": { "$value": "32px", "$type": "dimension" }
}
}
Radius, font size, icon sizes, dimensions
Same structure — each group has named steps, each step is a dimension token with a px value.
Writing to a file
// scripts/export-tokens.mjs
import { generateTheme, generateDtcgTokens } from 'salt-theme-gen';
import { writeFileSync, mkdirSync } from 'node:fs';
const theme = generateTheme({ preset: 'ocean' });
const { json } = generateDtcgTokens(theme);
mkdirSync('tokens', { recursive: true });
writeFileSync('tokens/salt-theme.tokens.json', json, 'utf8');
console.log('✓ tokens written to tokens/salt-theme.tokens.json');
The output is a single JSON file with light and dark keys at the top level.
Style Dictionary 4 integration
Style Dictionary 4 natively supports DTCG format. Point it at your exported file:
// sd.config.mjs
import StyleDictionary from 'style-dictionary';
const sd = new StyleDictionary({
source: ['tokens/salt-theme.tokens.json'],
platforms: {
css: {
transformGroup: 'css',
prefix: 'salt',
buildPath: 'dist/tokens/',
files: [
{
destination: 'light.css',
format: 'css/variables',
filter: (token) => token.filePath.includes('light'),
},
{
destination: 'dark.css',
format: 'css/variables',
filter: (token) => token.filePath.includes('dark'),
},
],
},
ios: {
transformGroup: 'ios-swift',
buildPath: 'dist/ios/',
files: [{ destination: 'StyleDictionary.swift', format: 'ios-swift/class.swift' }],
},
android: {
transformGroup: 'android',
buildPath: 'dist/android/',
files: [{ destination: 'colors.xml', format: 'android/colors' }],
},
},
});
await sd.buildAllPlatforms();
One source of truth → CSS, Swift, Android XML — all from generateDtcgTokens().
Token Studio integration
Token Studio is a Figma plugin that reads and writes DTCG-compatible JSON. To import your tokens:
- Run
node scripts/export-tokens.mjsto generatetokens/salt-theme.tokens.json - In Figma, open the Token Studio plugin
- Go to Settings → Sync and point it at your JSON file (via GitHub, URL, or local file)
- Token Studio will populate Figma variables from your generated values
When you regenerate the theme (new preset, different harmony), re-export and sync — Figma variables update automatically.
TypeScript types
import type { DtcgToken, DtcgColorToken, DtcgDimensionToken, DtcgGroup, DtcgTokensResult } from 'salt-theme-gen';
// DtcgColorToken: { $value: string; $type: 'color' }
// DtcgDimensionToken: { $value: string; $type: 'dimension' }
// DtcgToken: DtcgColorToken | DtcgDimensionToken
// DtcgGroup: { [key: string]: DtcgToken | DtcgGroup }
// (a node in the tree is either a token leaf or a group of more tokens)
// DtcgTokensResult:
// {
// light: DtcgGroup;
// dark: DtcgGroup;
// json: string;
// }
// Traversing the tree:
function isToken(v: unknown): v is DtcgToken {
return typeof v === 'object' && v !== null && '$value' in v && '$type' in v;
}
What changes between light and dark
The light and dark token trees have identical structure — same groups, same keys — but different $value fields for color tokens. Dimension tokens (spacing, radius, font size) have the same values in both modes because they are not mode-dependent.
const { light, dark } = generateDtcgTokens(theme);
const lightPrimary = (light.color as DtcgGroup).primary as DtcgToken;
const darkPrimary = (dark.color as DtcgGroup).primary as DtcgToken;
lightPrimary.$value // '#0077dc'
darkPrimary.$value // '#459af9'
Compared to generateCssVariables
Both functions export your theme tokens. Choose based on your consumer:
| Need | Use |
|---|---|
| Inject into a web page | generateCssVariables() → CSS string |
| Feed Style Dictionary | generateDtcgTokens() → JSON file |
| Import into Token Studio | generateDtcgTokens() → JSON file |
| Extend Tailwind config | generateTailwindConfig() → JS object |
| Build a native iOS/Android pipeline | generateDtcgTokens() → Style Dictionary |
| Quick prototype, no build step | generateCssVariables() → <style> inject |