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.
Modes: one build per mode
Section titled “Modes: one build per mode”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:
- Every file under
sets, in the order ofresolutionOrder. These are the same in every mode. - 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:
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.
Gradients
Section titled “Gradients”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'],Android
Section titled “Android”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
androidtransform group. It converts sizes as if they were rem and ignores the token’spxunit. Use the transforms below instead. spvsdp. DTCG has no “font size” type, so the format can’t say which sizes are text. Scalar keeps each variable’s Figma scopes in$extensionsascom.figma.scopes, and a small transform turns anything scoped to Font size intospand everything else intodp.- 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 (opacity0.4) are written as<integer>, which Android’s build rejects. The filter andresourceMapbelow 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();$root tokens
Section titled “$root tokens”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.
Things to know
Section titled “Things to know”- Typography extras (italic, text case, decoration) live in
$extensions. The CSSfontshorthand 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' } }.