Skip to content

Building with Style Dictionary

Style Dictionary builds Scalar’s output for the web, Android and iOS with the settings below.

The code on this page is tested automatically with Style Dictionary 5.7.1, against Scalar’s sample export (two multi-mode collections, every token and style type).

You need Style Dictionary 5 or later. Version 4 can’t read DTCG 2025.10’s object-form colors and dimensions.

Style Dictionary doesn’t read scalar.resolver.json yet (style-dictionary#1590), so you tell it which files make up each mode. The resolver lists them for you:

  1. Every file under sets, in the order of resolutionOrder. These are the same in every mode.
  2. Plus, for each multi-mode collection, the file for the mode you want, under modifiers.<collection>.contexts.<mode>.

For a file with one multi-mode collection (Theme: Light and Dark), that’s two builds:

build.mjs
import StyleDictionary from 'style-dictionary';
const shared = [
'tokens/primitives.tokens.json',
'tokens/theme.tokens.json', // the collection's shared file: don't leave it out
'tokens/paint-styles.tokens.json',
'tokens/text-styles.tokens.json',
'tokens/effect-styles.tokens.json',
];
const modes = [
{ file: 'tokens/theme.light.tokens.json', selector: ':root', out: 'light.css' },
{ file: 'tokens/theme.dark.tokens.json', selector: ':root[data-theme="dark"]', out: 'dark.css' },
];
for (const mode of modes) {
const sd = new StyleDictionary({
source: [...shared, mode.file],
platforms: {
css: {
transformGroup: 'css',
buildPath: 'build/css/',
files: [{ destination: mode.out, format: 'css/variables', options: { selector: mode.selector } }],
},
},
});
await sd.buildAllPlatforms();
}

Each CSS file holds the full set of tokens for its mode. Shared tokens repeat in both, which is right: a shared token that points to a mode token has a different value in each mode. Add outputReferences: true to the options to keep references as var() chains.

The common mistake is leaving out the collection’s shared file (theme.tokens.json). The mode files hold only the tokens that change by mode; without the shared file, the rest of the collection is missing and references to it break.

With more than one multi-mode collection, each build takes one mode file per collection, for example theme.dark.tokens.json plus density.compact.tokens.json.

A DTCG gradient is a list of color stops, and the css group has no transform for it: you’d get [object Object]. Add this transform next to the group. It writes the stops (#db2626 0%, #f59e0a 100%), which you wrap in linear-gradient(<angle>, …) yourself, since the format doesn’t carry the direction.

StyleDictionary.registerTransform({
name: 'scalar/gradient/css',
type: 'value',
transitive: true,
filter: (t) => (t.$type ?? t.type) === 'gradient',
transform: (t) =>
(t.$value ?? t.value)
.map(({ color: c, position }) => {
const css = typeof c === 'string' ? c
: c.alpha < 1 ? `rgb(${c.components.map((n) => Math.round(n * 255)).join(' ')} / ${c.alpha})` : c.hex;
return `${css} ${position * 100}%`;
})
.join(', '),
});
// in the css platform:
// transformGroup: 'css',
// transforms: ['scalar/gradient/css'],

Three settings matter on Android, and all of them are about the build, not the tokens:

  • Sizes come out 16× too big with the stock android transform group. It converts sizes as if they were rem and ignores the token’s px unit. Use the transforms below instead.
  • sp vs dp. DTCG has no “font size” type, so the format can’t say which sizes are text. Scalar keeps each variable’s Figma scopes in $extensions as com.figma.scopes, and a small transform turns anything scoped to Font size into sp and everything else into dp.
  • Leave out the types Android resources can’t hold. Text styles, shadows, gradients and other composite tokens come out as [object Object], and decimal numbers (opacity 0.4) are written as <integer>, which Android’s build rejects. The filter and resourceMap below handle both.
import StyleDictionary from 'style-dictionary';
StyleDictionary.registerTransform({
name: 'scalar/size/androidUnit',
type: 'value',
transitive: true,
filter: (t) => (t.$type ?? t.type) === 'dimension',
transform: (t) => {
const raw = t.$value ?? t.value;
const num = raw !== null && typeof raw === 'object' ? raw.value : parseFloat(raw);
const scopes = (t.$extensions ?? t.extensions ?? {})['com.figma.scopes'] ?? [];
return `${num.toFixed(2)}${scopes.includes('FONT_SIZE') ? 'sp' : 'dp'}`;
},
});
// Style Dictionary's Android output has no form for these composite types yet.
const composite = ['typography', 'shadow', 'gradient', 'duration', 'cubicBezier', 'border', 'transition'];
const sd = new StyleDictionary({
source: [/* the shared files + one mode file per collection, as above */],
platforms: {
android: {
transforms: ['attribute/cti', 'name/snake', 'color/hex8android', 'scalar/size/androidUnit'],
buildPath: 'build/android/',
files: [{
destination: 'tokens.xml',
format: 'android/resources',
filter: (t) => !composite.includes(t.$type ?? t.type),
// Numbers default to <integer>, which rejects decimals such as opacity 0.4.
options: { resourceMap: { dimension: 'dimen', color: 'color', string: 'string', number: 'string' } },
}],
},
},
});
await sd.buildAllPlatforms();

Use the tokens that carry the Font size scope for text. The scope is on the variable itself, not inherited through references. If your text sizes are semantic variables scoped to Font size that point to unscoped primitives, the semantic ones become sp and the primitives dp. Use the semantic ones for text.

Keep the stock ios-swift group and set basePxFontSize: 1. The sizes are already px, so this turns the rem conversion into a 1:1 pass-through (without it, sizes are 16× too big).

As on Android, leave out the composite types, and quote text values (font families, text variables), which the group writes bare. The result is a Swift file that compiles.

import StyleDictionary from 'style-dictionary';
StyleDictionary.registerTransform({
name: 'scalar/swift/string',
type: 'value',
filter: (t) => ['string', 'fontFamily'].includes(t.$type ?? t.type),
transform: (t) => {
const v = t.$value ?? t.value;
return JSON.stringify(Array.isArray(v) ? v[0] : v);
},
});
// Style Dictionary's Swift output has no form for these composite types yet.
const composite = ['typography', 'shadow', 'gradient', 'duration', 'cubicBezier', 'border', 'transition'];
const sd = new StyleDictionary({
source: [/* the shared files + one mode file per collection, as above */],
platforms: {
ios: {
transformGroup: 'ios-swift',
transforms: ['scalar/swift/string'],
basePxFontSize: 1,
buildPath: 'build/ios/',
files: [{
destination: 'Tokens.swift',
format: 'ios-swift/class.swift',
options: { className: 'Tokens' },
filter: (t) => !composite.includes(t.$type ?? t.type),
}],
},
},
});
await sd.buildAllPlatforms();

A token with tokens nested under it is exported as its group’s $root (see Known limitations). Style Dictionary 5.7.1 handles it fully: the token is named after its group (--text-warning), and references to it work with outputReferences.

Older 5.x versions (checked on 5.5) named it …-root and wrote references to it as raw {text.warning.$root} text, which is invalid CSS. If you see either, upgrade.

  • Typography extras (italic, text case, decoration) live in $extensions. The CSS font shorthand can’t carry them, and Style Dictionary warns about this on every build (“Unknown CSS Font Shorthand properties”). Read them in a custom transform or format if you need them. See Known limitations.
  • Library variables that aren’t in the exported file show up as broken references. Style Dictionary reports them and can still build the rest if you set log: { errors: { brokenReferences: 'console' } }.