Transformer
How to turn the plugin's token JSON into CSS, SCSS, Swift, UIKit, Kotlin, Android XML and a JSON manifest with the Transformer CLI: quick start, commands, the YAML configuration reference and typical setups.
Transformer (@sxl-studio/token-transformer, version 3.4.1) is a command-line tool that turns the token JSON files exported by the plugin into code. It runs on a developer's computer and in CI, is configured with one YAML file and rebuilds only what changed. It needs Node.js 20 or newer.
| Platform | What you get |
|---|---|
css | Custom properties inside :root or a selector of your choice |
scss | $variables; theme files @use their root file |
swift | SwiftUI constants and typed specs, marked Sendable |
uikit | UIKit constants and typed specs, marked Sendable |
kotlin | Kotlin constants and data classes for Compose |
xml | Android resources: colors.xml, dimens.xml, strings.xml, bools.xml, integers.xml, floats.xml and drawables |
manifest | JSON with token metadata for documentation sites, token viewers and audits |
For a one-off export without a terminal use the plugin itself, see Tokens to code. Transformer reads the same files and the same config.json.
Quick start in 5 minutes
You need Node.js 20 or newer and a folder with the token JSON files and config.json from the plugin. Where that folder comes from is described in Token sources and synchronization.
- Install the package in the project:
npm install --save-dev @sxl-studio/token-transformer.pnpm add -Dandyarn add -Dwork the same way. - Create a starter config:
npx sxl-transform init. The filesxl-transform.config.yamlappears in the current folder with examples for every platform. - Open the file. Point
source.tokenDirto the token folder and replace the collection and mode names intokenSetswith the ones from yourconfig.json. Delete the outputs you do not need. - Check the config:
npx sxl-transform validate-config. - Build:
npx sxl-transform sync.
npm install --save-dev @sxl-studio/token-transformer
npx sxl-transform init
npx sxl-transform validate-config
npx sxl-transform sync
The generated files appear in the outputDir of every output. Next to the config the tool writes sxl-transform.config.state.json: with it the next sync rebuilds only the outputs whose tokens changed. The state file is safe to leave out of Git: without it the next run is a full rebuild.
Commands
| Command | What it does |
|---|---|
sxl-transform sync | Converts tokens and writes the files. It is the default command, so sxl-transform --config ./x.yaml also runs it |
sxl-transform validate-config | Checks the YAML config and the paths in it, writes nothing. Accepts only --config |
sxl-transform init | Creates a starter config. --path <path> chooses the file, --force overwrites an existing one |
sxl-transform help [command] | General help or help for one command. --help and -h do the same |
sxl-transform version | Prints the installed version. --version and -v do the same |
Options of sync
| Option | What it does | Default |
|---|---|---|
--config <path> | Path to the YAML config | sxl-transform.config.yaml |
--mode <mode> | smart rebuilds only changed outputs, force rebuilds everything | smart |
--force | Same as --mode force | |
--preserve-existing | Retain old tokens, add new ones and update matching declarations in all formats | off |
--state-file <path> | Where to keep the state file | <config-name>.state.json next to the config |
--only-output <id> | Build only this output. Repeat for several | all outputs |
--only-file <glob> | Build only the matching output files. Repeat for several. css-app:modes/dark.css limits the pattern to one output | all files |
--issue-action <mode> | What to do when problems are found: ask, debug-stop, debug-continue, autofix or skip. --issues is an alias | ask |
--debug-report [path] | Write the debug report even for a clean run, optionally to the given path | only when there are problems |
--debug-file <path> | Path of the debug report | <config-name>.debug.md in the current folder |
--explain | Print why every output was rebuilt or skipped | |
--watch | Keep running and rebuild when tokens or the config change. Ctrl+C stops it | |
--no-state-lock | Disable the lock that protects the state file from parallel runs |
sync strategies: smart, force and preserve
The sync command has two rebuild modes: smart and force. Preserve is a separate policy that merges generated data with existing output files.
| Strategy | What happens | When to use it |
|---|---|---|
smart | Adds and updates tokens and removes those absent from JSON. Writes only files whose content changed | Normal day-to-day synchronization |
force | Regenerates and writes every selected file. Tokens absent from JSON are removed by default | A complete verification rebuild or file recovery |
preserve | Adds and updates tokens but retains those absent from JSON. Writes only changed files | A gradual migration while older consumers still use previous names |
Important preserve is not a third --mode value. There is no --mode preserve command: use --mode smart --preserve-existing. The flag belongs to the invocation and is not added to YAML.
# Normal synchronization: JSON remains the source of truth
npx sxl-transform sync --config ./sxl-transform.config.yaml --mode smart --issue-action debug-stop
# Rebuild every configured output
npx sxl-transform sync --config ./sxl-transform.config.yaml --mode force --issue-action debug-stop
# Migration: retain old declarations, update matches and add new ones
npx sxl-transform sync --config ./sxl-transform.config.yaml --mode smart --preserve-existing --issue-action debug-stop
A neutral scripts setup for a project:
{
"scripts": {
"tokens:sync": "sxl-transform sync --config ./sxl-transform.config.yaml --mode smart --issue-action debug-stop",
"tokens:force": "sxl-transform sync --config ./sxl-transform.config.yaml --mode force --issue-action debug-stop",
"tokens:preserve": "sxl-transform sync --config ./sxl-transform.config.yaml --mode smart --preserve-existing --issue-action debug-stop"
}
}
For example, an output used to contain --color-action and --color-legacy. After a refactor, the value of color.action changes, color.legacy is removed from JSON and color.accent is added. Normal smart and force update action, add accent and remove legacy. Preserve updates action, adds accent and retains legacy. If a token is renamed, preserve keeps the previous name and adds the new one; a later normal smart rebuild removes the previous name.
Preserve reads old declarations from output files, so do not clean the output directory before using it. For a scoped run, add the same --only-output and --only-file options listed above. Changes outside the selected scope remain pending for the next full sync.
# only two outputs
npx sxl-transform sync --only-output css-app --only-output swift-app
# only the dark theme file of the css-app output
npx sxl-transform sync --only-file "css-app:modes/dark.css"
# CI: never wait for input, skip broken tokens
npx sxl-transform sync --issue-action skip
How preserve matches declarations
For CSS, SCSS, Swift, UIKit, Kotlin, Android XML and manifests, matching uses the public name and declaration scope:
| Format | Matching scope |
|---|---|
| CSS | Case-sensitive name, selector and enclosing at-rules |
| SCSS | Sass variable name within one file/module; hyphens and underscores are equivalent |
| Swift / UIKit / Kotlin | Namespace and property name; Kotlin also includes the package |
| Android XML | Resource type, name and qualifier such as values-night; drawables are replaced as whole files |
| Manifest | Name, collection and mode; moving source JSON between files does not create a duplicate |
Old declarations replaced by current ones in another file within the same scope are not duplicated. Separate Sass modules and native namespaces remain independent: changing NewTokens.size does not update OldTokens.size. Stale output files and their imports through indexes.includeGenerated remain available. Native namespaces and shared helper types have one owner within each compilation scope.
The flag is invocation-only; it has no YAML setting. It works with --mode smart, --force, --watch, --only-output and --only-file. Running without it restores normal source-based generation for the selected outputs; retained values are removed when those files are rebuilt. options.removeStaleOutputs: false separately controls retention of entire stale files.
Without a state file, existing files are recovered through configured generation paths and split patterns. Arbitrary files outside those patterns are not discovered. Values already lost from existing outputs cannot be recovered. Retained outputs do not replace JSON sources during alias resolution.
Malformed or unsupported files stop the run before any outputs are written. The handlers support Transformer-generated structures; arbitrary custom formatter code may require adaptation. CSS anonymous @layer blocks cannot be merged because they lack stable identities. Source-token warnings still follow --issue-action. An empty glob caused by confirmed removal of previous sources becomes informational when existing outputs are retained.
Smart mode and the state file
sync compares the content of the token files with the state file and rebuilds only the outputs that depend on changed files. The generated files are checked too: a file that was deleted or edited by hand is written again. Files whose tokens disappeared are deleted, unless options.removeStaleOutputs is false.
In 3.4.1, smart writes only files whose actual content differs, including after config or preservation-policy changes. --mode force regenerates and writes all selected outputs. --mode smart --preserve-existing retains declarations absent from JSON; a rename keeps both old and new names. Preliminary directory cleanup is unnecessary and removes the data that preserve needs to retain.
What to expect:
- The first run and a run without a state file rebuild everything. Upgrades that change generated output, including 3.4.1, invalidate older state once; later unchanged runs skip generation again.
- A run limited with
--only-outputor--only-filedoes not hide the other changes: the next full run picks them up. With--only-filestale files outside the selection are kept. - While
syncruns, the file<state>.lockblocks a second parallel run. A lock older than 10 minutes is taken over automatically. - If your own script rewrites the generated files after
sync, the next run rebuilds them, because their content no longer matches the state. Run such scripts beforesyncor write to another folder. --watchwatches the token folder, the YAML config andconfig.json, ignores the output folders and never asks questions: when problems are found it writes the debug report and continues.
Which tokens are converted
CSS, SCSS, Swift, UIKit and Kotlin recognize the following token groups. The accepted value shapes and generated fields depend on the target platform; see the limitations below.
| Group | Types |
|---|---|
| Colours and fills | color, gradient, fill, img, opacity |
| Dimensions | dimension, number, spacing, sizing |
| Borders | border, borderWidth, borderRadius, strokeStyle |
| Typography | typography, fontFamily, fontWeight, fontStyle, fontSize, lineHeight, letterSpacing, paragraphSpacing, paragraphIndent, textCase, textDecoration |
| Effects | shadow, blur, backdrop-blur, effects |
| Motion | transition, duration, cubicBezier, easing |
| Other | boolean, text |
The remaining types:
-
glassis recognized and retained in the manifest. Typed outputs do not have a glass-specific representation; standalone glass values are unsupported, and glass layers inside effects produce diagnostics. -
customtokens are kept as they are. The manifest exports their$valueverbatim; the other platforms skip them with a warning, because the value has no fixed shape. -
template,compositionandgriddescribe Figma structures, not values, and are never converted.options.unsupportedTypesdecides whether they produce an error, a warning or are skipped silently.
Tokens hidden in Figma ($extensions["figma.hide"]) are exported by default, because visible tokens often reference them. excludeHidden: true on an output drops them.
References ({color.brand}), numeric expressions and supported figma.modify colour modifiers are resolved before generation. Math runs only for numeric types; a text token such as "37-37" is never calculated. The source format is described in Token JSON format; support for a Figma field does not imply that every output can render it.
Value shapes and platform limits
- Dimension and duration tokens accept DTCG
{value, unit}objects. DTCG colours accept numeric components insrgb,display-p3,hsl,oklchandlch, or a supportedhexfallback. Other colour spaces andnonecomponents need a fallback; invalid explicit alpha is rejected. - CSS/SCSS accept gradient strings, including repeating gradients, shadow shorthand and image URLs. Figma video and pattern fills and standalone glass values have no complete conversion. Spring easing is an approximation and produces a warning.
- SwiftUI, UIKit and Kotlin export values and typed specifications. Linear gradient strings retain direction and double-position stops. Complex radial/conic geometry, repeating gradients, automatic typography line height and Figma video/pattern/glass fills remain unsupported value shapes.
- Image filters and transforms, progressive blur geometry, noise/texture/glass effect layers and font variation axes are not reproduced by the generated value/spec formats. Diagnostics identify fields that would otherwise be omitted.
- A manifest retains the authored data for allowed token types, including
custom. It does not render effects. Keep it alongside typed outputs when you need the original details.
Unsupported values produce diagnostics; they are not proof of a successful conversion. The default policy stops on errors. Use a documented warning/skip policy only when your consumer can intentionally omit those values.
Typography
A typography token needs fontFamily, fontWeight, fontSize and lineHeight. CSS and SCSS get one shorthand value in the font order: style, weight, size/line-height, family; fields outside that shorthand produce warnings. Swift, UIKit and Kotlin get a typed spec with fields including letterSpacing, textCase, textDecoration and verticalTrim. Paragraph spacing, paragraph indent and font variation axes are not emitted. XML splits supported fields into resources: <name>_font_family, <name>_font_weight, <name>_font_size, <name>_line_height, <name>_letter_spacing, <name>_text_case, <name>_text_decoration.
Android XML
XML is limited to native resources. The output of an XML file mapping is a folder, for example values; drawables go to the sibling drawable folder.
| Token | Resource |
|---|---|
color | colors.xml; a linear-gradient(...) value becomes a drawable |
gradient, fill, img | drawable/<name>.xml |
dimension, spacing, sizing, borderWidth, borderRadius, paragraphSpacing, paragraphIndent | dimens.xml in dp |
fontSize, lineHeight, letterSpacing | dimens.xml in sp |
text, fontFamily, fontWeight, fontStyle, textCase, textDecoration | strings.xml |
boolean | bools.xml |
number (integer), duration (milliseconds), opacity (0 to 100) | integers.xml |
number with a fraction | floats.xml as <item type="dimen" format="float">, read with ResourcesCompat.getFloat |
typography | Split into strings.xml and dimens.xml, see above |
Shadows, blurs, glass, effects, transitions, easing curves, border and strokeStyle have no resource equivalent: they are listed in the diagnostics and skipped.
Configuration reference
The config is YAML only, version: 1. Relative paths are resolved from the config file. Unknown keys are errors, so a typo is caught by validate-config.
Top level
| Key | What it does | Default |
|---|---|---|
version | Config format version, always 1 | required |
extends | YAML files whose content is merged into this one, for sharing a base between configs | [] |
source | Where the token files are | required |
projectConfig | Inline description of collections and modes for projects without config.json | none |
options | Global options | see below |
tokenSets | Named sets of tokens, at least one | required |
outputs | What to generate, at least one | required |
source
| Key | What it does | Default |
|---|---|---|
tokenDir | Folder with the token JSON files | required |
configFile | The plugin's config.json. If omitted, <tokenDir>/config.json is used when it exists; null turns it off | auto |
include | Globs of files to read | ["**/*.json"] |
exclude | Globs to skip | ["config.json", "**/diff-id*.json"] |
projectConfig repeats the structure of config.json: collections with name and modes, and in every mode name plus files (a map of file path to enabled). Selecting tokens by collection and mode needs one of the two sources.
options
| Key | What it does | Default |
|---|---|---|
remBase | Pixels in one rem. Converts rem and em to numbers for Swift, UIKit, Kotlin and XML: 1.5rem gives 24 | 16 |
collisionStrategy | Two files define the same token path: error stops the run, suffix appends __dup_N, namespace-by-file and namespace-by-mode prefix the path with the file or mode name | error |
maxAliasDepth | Longest allowed chain of references | 20 |
removeStaleOutputs | Delete generated files whose tokens disappeared | true |
unsupportedTypes.default | Action for a type the platform cannot emit: error, warn or skip | warn |
unsupportedTypes.types | Per-type override, for example template: skip | {} |
tokenSets[]
| Key | What it does | Default |
|---|---|---|
id | Name used in outputs | required |
selectors | Which files make up the set, merged in the listed order | required |
unresolvedAliases | References that point nowhere: error, warn or ignore | error |
A selector picks files either by collection and mode from config.json, or by globs:
| Key | What it does | Default |
|---|---|---|
collection, mode | Collection and mode from config.json or projectConfig, always together | |
files, include | Globs relative to tokenDir | |
exclude | Globs removed from the set and from reference resolution | [] |
includeRefs | Also load the collections this mode refers to, so that references resolve | true |
refModeMap | Which mode to take for each referenced collection, for example Core: Default | {} |
tokenSets:
- id: app-root
selectors:
- collection: Core
mode: Default
- collection: Themes
mode: Light
refModeMap:
Core: Default
- id: components
unresolvedAliases: warn
selectors:
- files: ["components/**/*.style.json"]
outputs[]
| Key | What it does | Default |
|---|---|---|
id | Name for --only-output and messages | required |
platform | css, scss, swift, uikit, kotlin, xml or manifest | required |
outputDir | Folder for the generated files | required |
prefix, suffix | Added to every token name: prefix: ds gives --ds-color-primary | none |
resolveAliases | true writes final values, false keeps references such as var(--other) | false for css and scss, true for the rest |
showDescriptions | Token descriptions as comments | true |
splitEffects | Kept for compatibility. Effect tokens that mix shadows and blurs are always written as -shadow, -filter and -backdrop-filter variables | true |
excludeHidden | Drop tokens hidden in Figma. Does not affect manifest | false |
includePrelude | Swift, UIKit and Kotlin: write the shared SXL* support types into the file. Set false when another file already provides them | true |
codeSyntax | Naming fallback, see Naming | none |
files | Static files and per-source-file splits | [] |
bundles | Several source files in one output file | [] |
bundlesFromCollections | One bundle per collection from config.json | [] |
indexes | CSS and SCSS entry files | [] |
options | Platform options: manifest options, customFormatters | {} |
An output needs at least one of files, bundles, bundlesFromCollections or indexes.
outputs[].files[]
| Key | What it does | Default |
|---|---|---|
tokenSet | Which set to write | required |
output | File path inside outputDir; for xml a folder such as values | output or splitBySourceFile is required |
splitBySourceFile | One file per source file, see below | none |
selector | CSS only: the wrapping selector, a string or a list. Other platforms ignore it with a warning | :root |
prefix, suffix | Override the output affixes for this file | output values |
filter | Keep only some tokens: types (type names), paths (path prefixes), excludePaths | none |
options | Per-file options, for example manifest options | none |
splitBySourceFile:
| Key | What it does | Default |
|---|---|---|
include, exclude | Which source files get their own output file | ["**/*.style.json"], [] |
outputPattern | File name with placeholders | required |
prefixPattern, suffixPattern | Token name affixes with the same placeholders | none |
excludeBundledSources | Skip files that already go into bundles or bundlesFromCollections | false |
excludeFromCollections | Skip files of the listed collections: mode plus include or exclude names | none |
Placeholders: {sourceFile} (relative path), {sourceDir}, {sourceBase} (name with extension), {sourceName} (without extension), {sourceStem} (also without .style), {component} (the folder after components/, otherwise the stem), {fileName} (same as {sourceStem}). selector accepts the same placeholders.
bundles[] and bundlesFromCollections[]
A bundle collects several source files of one token set into one output file. It goes through the same pipeline as a regular file, so selector, filter, prefix, suffix and options work the same way.
| Key | bundles[] | bundlesFromCollections[] |
|---|---|---|
tokenSet | required | required |
output | file path, required | built from outputPattern |
include, exclude | source file globs, include required | collection names or patterns; collections: {include, exclude} is the nested form |
mode | mode whose enabled files are taken, required | |
fileInclude, fileExclude | globs applied to the collection files | |
outputPattern | {collection}, {collectionKebab}, {collectionSnake}, {collectionLower}, {mode}, {modeKebab}, {modeSnake}, {modeLower} | |
strict | true turns a missing collection or mode into an error instead of a warning |
indexes[]
An index is an entry file that imports the generated files: @import "..."; for CSS, @use "..." as *; for SCSS. Other platforms report an error for indexes.
| Key | What it does | Default |
|---|---|---|
output | Index file path inside outputDir | required |
imports | Explicit imports, relative to the index file | [] |
includeGenerated | Globs over the files generated by the same output | [] |
exclude | Globs to leave out | [] |
sort | generated-order or alpha | generated-order |
skipMissing | Do not fail when an explicit import does not exist | false |
strict | Fail when an includeGenerated pattern matches nothing | false |
imports or includeGenerated must be set.
Manifest options
Set them in options of a manifest output or of one of its files.
| Key | What it does | Default |
|---|---|---|
schemaVersion | Version string written at the top of the file | "1.0" |
includeResolvedValue | Add resolvedValue | true |
includeOriginalValue | Add the original $value | true |
includeReferences | List the references found in the original value | true |
includeSource | Add collection, mode, tokenSet and level | true |
includeExtensions | Add $extensions | false |
includePrivate | Keep tokens hidden in Figma | true |
cssVarPrefix, cssVarSuffix | Affixes for the cssVar field only | output prefix and suffix |
groupBy | flat, collection, mode or file | flat |
Every entry has name, cssVar, path, type, value, sourceFile, references and weight. level and weight are read from the extensions level, sxl.level and sxl.weight; without them they are null.
Reference names also respect the target token's Code Syntax when the target is emitted into another file. Targets are resolved within the selected mode and its configured dependencies, so references from different modes are not mixed.
Naming
Names are built from the token path plus the output prefix and suffix. A token with Code Syntax in $extensions keeps its own name: the Web key is used for CSS, SCSS and manifest, iOS for Swift and UIKit, Android for Kotlin and XML. See Scopes and Code Syntax.
codeSyntax on an output sets a fallback template and the priority:
| Key | Values | Default |
|---|---|---|
source | extension-first (the token wins), config-first (the template wins), extension-only, config-only | extension-first |
template | {var(--css-variable)}, {$sass-variable}, {@less-variable}, {UpperCamelCase}, {lowerCamelCase}, {UPPER_SNAKE_CASE}, {lower_snake_case}. Required with config-first and config-only | none |
Typical setups
Root plus theme files (CSS)
outputs:
- id: css-app
platform: css
outputDir: ./ds/css
files:
- tokenSet: app-root
output: root.css
- tokenSet: app-dark
output: themes/dark.css
selector: "[data-theme='dark']"
indexes:
- output: index.css
imports: ["./root.css", "./themes/dark.css"]
References stay as var(--...), so the theme switches with the selector alone.
One CSS file per component
outputs:
- id: css-components
platform: css
outputDir: ./ds/components
files:
- tokenSet: components
splitBySourceFile:
include: ["components/**/*.style.json"]
outputPattern: "{component}/{component}.css"
prefixPattern: "c-{component}-"
selector:
- ":root"
- "[data-component='{component}']"
A new components/WAccordion/WAccordion.style.json gets its own WAccordion/WAccordion.css on the next run without any change to the config.
Custom formatters
When one token type must look different on one platform, attach a JavaScript module to the output:
outputs:
- id: css-app
platform: css
outputDir: ./ds/css
files:
- tokenSet: app-root
output: root.css
options:
customFormatters:
module: ./transform-hooks.mjs
exportName: plugin
The module exports an object with tokenTypeFormatters.<platform>.<type> functions. A formatter receives the token and returns undefined to keep the default, null to drop the token, or a replacement token. An optional platformEmitters.<platform> function replaces the rendering of the whole file. The CustomFormatterPlugin type is exported by the package.
Problems and the debug report
When sync finds problems (a reference that points nowhere, a broken reference syntax, two tokens with one path, a file that does not parse), it asks what to do:
- Create the debug file and stop.
- Create the debug file and continue.
- Try to auto-fix simple reference syntax and continue.
- Skip broken tokens and continue.
--issue-action gives the answer in advance. Without a terminal, for example in CI, ask behaves as debug-stop; in --watch mode as debug-continue. The debug report <config-name>.debug.md lists every problem with the file and the token path, plus the generated files. It is always written when there are problems; --debug-report writes it for a clean run too.
Recommended package.json scripts:
{
"scripts": {
"tokens:validate": "sxl-transform validate-config --config ./sxl-transform.config.yaml",
"tokens:build": "sxl-transform sync --config ./sxl-transform.config.yaml --issue-action debug-stop",
"tokens:build:ci": "sxl-transform sync --config ./sxl-transform.config.yaml --issue-action skip"
}
}
If something does not work
| What happened | What to do |
|---|---|
PROJECT_CONFIG_REQUIRED | A selector uses collection and mode, but there is no config.json in tokenDir and no projectConfig in the YAML. Point source.configFile to the file or describe the collections inline |
Config already exists after init | Add --force or choose another --path |
| Warnings about unresolved references in a theme or component set | The referenced collection is not in the set. Add it as a selector, keep includeRefs: true and fix its mode with refModeMap |
| Tokens are missing from a file | Read the diagnostics: template, composition and grid are never converted, custom goes only to the manifest, XML skips types without a resource equivalent |
sync reports that the state lock is held | Another run is in progress. Wait, or delete <state>.lock if that process died; a lock older than 10 minutes is taken over automatically |
| Nothing is rebuilt although tokens changed | Run with --explain to see the reasons or with --force for a full rebuild. Check that the changed file matches source.include |
| Files are rebuilt on every run | A script rewrites the generated files after sync. Run it before sync or write elsewhere |
SCSS: Undefined variable | Theme files take variables from the nearest root.scss. Keep them next to the root file and load the root file before component files |
| Names differ from Dev Mode | Set Code Syntax on the tokens or codeSyntax on the output; check prefix and suffix |
CSS keeps var(--...) but you need values | Set resolveAliases: true on the output |