Utilities

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.

PlatformWhat you get
cssCustom properties inside :root or a selector of your choice
scss$variables; theme files @use their root file
swiftSwiftUI constants and typed specs, marked Sendable
uikitUIKit constants and typed specs, marked Sendable
kotlinKotlin constants and data classes for Compose
xmlAndroid resources: colors.xml, dimens.xml, strings.xml, bools.xml, integers.xml, floats.xml and drawables
manifestJSON 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.

  1. Install the package in the project: npm install --save-dev @sxl-studio/token-transformer. pnpm add -D and yarn add -D work the same way.
  2. Create a starter config: npx sxl-transform init. The file sxl-transform.config.yaml appears in the current folder with examples for every platform.
  3. Open the file. Point source.tokenDir to the token folder and replace the collection and mode names in tokenSets with the ones from your config.json. Delete the outputs you do not need.
  4. Check the config: npx sxl-transform validate-config.
  5. Build: npx sxl-transform sync.
BASH
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

CommandWhat it does
sxl-transform syncConverts tokens and writes the files. It is the default command, so sxl-transform --config ./x.yaml also runs it
sxl-transform validate-configChecks the YAML config and the paths in it, writes nothing. Accepts only --config
sxl-transform initCreates 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 versionPrints the installed version. --version and -v do the same

Options of sync

OptionWhat it doesDefault
--config <path>Path to the YAML configsxl-transform.config.yaml
--mode <mode>smart rebuilds only changed outputs, force rebuilds everythingsmart
--forceSame as --mode force
--preserve-existingRetain old tokens, add new ones and update matching declarations in all formatsoff
--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 severalall outputs
--only-file <glob>Build only the matching output files. Repeat for several. css-app:modes/dark.css limits the pattern to one outputall files
--issue-action <mode>What to do when problems are found: ask, debug-stop, debug-continue, autofix or skip. --issues is an aliasask
--debug-report [path]Write the debug report even for a clean run, optionally to the given pathonly when there are problems
--debug-file <path>Path of the debug report<config-name>.debug.md in the current folder
--explainPrint why every output was rebuilt or skipped
--watchKeep running and rebuild when tokens or the config change. Ctrl+C stops it
--no-state-lockDisable 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.

StrategyWhat happensWhen to use it
smartAdds and updates tokens and removes those absent from JSON. Writes only files whose content changedNormal day-to-day synchronization
forceRegenerates and writes every selected file. Tokens absent from JSON are removed by defaultA complete verification rebuild or file recovery
preserveAdds and updates tokens but retains those absent from JSON. Writes only changed filesA 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.

BASH
# 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:

JSON
{
  "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.

BASH
# 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:

FormatMatching scope
CSSCase-sensitive name, selector and enclosing at-rules
SCSSSass variable name within one file/module; hyphens and underscores are equivalent
Swift / UIKit / KotlinNamespace and property name; Kotlin also includes the package
Android XMLResource type, name and qualifier such as values-night; drawables are replaced as whole files
ManifestName, 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-output or --only-file does not hide the other changes: the next full run picks them up. With --only-file stale files outside the selection are kept.
  • While sync runs, the file <state>.lock blocks 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 before sync or write to another folder.
  • --watch watches the token folder, the YAML config and config.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.

GroupTypes
Colours and fillscolor, gradient, fill, img, opacity
Dimensionsdimension, number, spacing, sizing
Bordersborder, borderWidth, borderRadius, strokeStyle
Typographytypography, fontFamily, fontWeight, fontStyle, fontSize, lineHeight, letterSpacing, paragraphSpacing, paragraphIndent, textCase, textDecoration
Effectsshadow, blur, backdrop-blur, effects
Motiontransition, duration, cubicBezier, easing
Otherboolean, text

The remaining types:

  • glass is 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.

  • custom tokens are kept as they are. The manifest exports their $value verbatim; the other platforms skip them with a warning, because the value has no fixed shape.

  • template, composition and grid describe Figma structures, not values, and are never converted. options.unsupportedTypes decides 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 in srgb, display-p3, hsl, oklch and lch, or a supported hex fallback. Other colour spaces and none components 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.

TokenResource
colorcolors.xml; a linear-gradient(...) value becomes a drawable
gradient, fill, imgdrawable/<name>.xml
dimension, spacing, sizing, borderWidth, borderRadius, paragraphSpacing, paragraphIndentdimens.xml in dp
fontSize, lineHeight, letterSpacingdimens.xml in sp
text, fontFamily, fontWeight, fontStyle, textCase, textDecorationstrings.xml
booleanbools.xml
number (integer), duration (milliseconds), opacity (0 to 100)integers.xml
number with a fractionfloats.xml as <item type="dimen" format="float">, read with ResourcesCompat.getFloat
typographySplit 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

KeyWhat it doesDefault
versionConfig format version, always 1required
extendsYAML files whose content is merged into this one, for sharing a base between configs[]
sourceWhere the token files arerequired
projectConfigInline description of collections and modes for projects without config.jsonnone
optionsGlobal optionssee below
tokenSetsNamed sets of tokens, at least onerequired
outputsWhat to generate, at least onerequired

source

KeyWhat it doesDefault
tokenDirFolder with the token JSON filesrequired
configFileThe plugin's config.json. If omitted, <tokenDir>/config.json is used when it exists; null turns it offauto
includeGlobs of files to read["**/*.json"]
excludeGlobs 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

KeyWhat it doesDefault
remBasePixels in one rem. Converts rem and em to numbers for Swift, UIKit, Kotlin and XML: 1.5rem gives 2416
collisionStrategyTwo 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 nameerror
maxAliasDepthLongest allowed chain of references20
removeStaleOutputsDelete generated files whose tokens disappearedtrue
unsupportedTypes.defaultAction for a type the platform cannot emit: error, warn or skipwarn
unsupportedTypes.typesPer-type override, for example template: skip{}

tokenSets[]

KeyWhat it doesDefault
idName used in outputsrequired
selectorsWhich files make up the set, merged in the listed orderrequired
unresolvedAliasesReferences that point nowhere: error, warn or ignoreerror

A selector picks files either by collection and mode from config.json, or by globs:

KeyWhat it doesDefault
collection, modeCollection and mode from config.json or projectConfig, always together
files, includeGlobs relative to tokenDir
excludeGlobs removed from the set and from reference resolution[]
includeRefsAlso load the collections this mode refers to, so that references resolvetrue
refModeMapWhich mode to take for each referenced collection, for example Core: Default{}
YAML
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[]

KeyWhat it doesDefault
idName for --only-output and messagesrequired
platformcss, scss, swift, uikit, kotlin, xml or manifestrequired
outputDirFolder for the generated filesrequired
prefix, suffixAdded to every token name: prefix: ds gives --ds-color-primarynone
resolveAliasestrue writes final values, false keeps references such as var(--other)false for css and scss, true for the rest
showDescriptionsToken descriptions as commentstrue
splitEffectsKept for compatibility. Effect tokens that mix shadows and blurs are always written as -shadow, -filter and -backdrop-filter variablestrue
excludeHiddenDrop tokens hidden in Figma. Does not affect manifestfalse
includePreludeSwift, UIKit and Kotlin: write the shared SXL* support types into the file. Set false when another file already provides themtrue
codeSyntaxNaming fallback, see Namingnone
filesStatic files and per-source-file splits[]
bundlesSeveral source files in one output file[]
bundlesFromCollectionsOne bundle per collection from config.json[]
indexesCSS and SCSS entry files[]
optionsPlatform options: manifest options, customFormatters{}

An output needs at least one of files, bundles, bundlesFromCollections or indexes.

outputs[].files[]

KeyWhat it doesDefault
tokenSetWhich set to writerequired
outputFile path inside outputDir; for xml a folder such as valuesoutput or splitBySourceFile is required
splitBySourceFileOne file per source file, see belownone
selectorCSS only: the wrapping selector, a string or a list. Other platforms ignore it with a warning:root
prefix, suffixOverride the output affixes for this fileoutput values
filterKeep only some tokens: types (type names), paths (path prefixes), excludePathsnone
optionsPer-file options, for example manifest optionsnone

splitBySourceFile:

KeyWhat it doesDefault
include, excludeWhich source files get their own output file["**/*.style.json"], []
outputPatternFile name with placeholdersrequired
prefixPattern, suffixPatternToken name affixes with the same placeholdersnone
excludeBundledSourcesSkip files that already go into bundles or bundlesFromCollectionsfalse
excludeFromCollectionsSkip files of the listed collections: mode plus include or exclude namesnone

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.

Keybundles[]bundlesFromCollections[]
tokenSetrequiredrequired
outputfile path, requiredbuilt from outputPattern
include, excludesource file globs, include requiredcollection names or patterns; collections: {include, exclude} is the nested form
modemode whose enabled files are taken, required
fileInclude, fileExcludeglobs applied to the collection files
outputPattern{collection}, {collectionKebab}, {collectionSnake}, {collectionLower}, {mode}, {modeKebab}, {modeSnake}, {modeLower}
stricttrue 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.

KeyWhat it doesDefault
outputIndex file path inside outputDirrequired
importsExplicit imports, relative to the index file[]
includeGeneratedGlobs over the files generated by the same output[]
excludeGlobs to leave out[]
sortgenerated-order or alphagenerated-order
skipMissingDo not fail when an explicit import does not existfalse
strictFail when an includeGenerated pattern matches nothingfalse

imports or includeGenerated must be set.

Manifest options

Set them in options of a manifest output or of one of its files.

KeyWhat it doesDefault
schemaVersionVersion string written at the top of the file"1.0"
includeResolvedValueAdd resolvedValuetrue
includeOriginalValueAdd the original $valuetrue
includeReferencesList the references found in the original valuetrue
includeSourceAdd collection, mode, tokenSet and leveltrue
includeExtensionsAdd $extensionsfalse
includePrivateKeep tokens hidden in Figmatrue
cssVarPrefix, cssVarSuffixAffixes for the cssVar field onlyoutput prefix and suffix
groupByflat, collection, mode or fileflat

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:

KeyValuesDefault
sourceextension-first (the token wins), config-first (the template wins), extension-only, config-onlyextension-first
template{var(--css-variable)}, {$sass-variable}, {@less-variable}, {UpperCamelCase}, {lowerCamelCase}, {UPPER_SNAKE_CASE}, {lower_snake_case}. Required with config-first and config-onlynone

Typical setups

Root plus theme files (CSS)

YAML
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

YAML
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:

YAML
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:

  1. Create the debug file and stop.
  2. Create the debug file and continue.
  3. Try to auto-fix simple reference syntax and continue.
  4. 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:

JSON
{
  "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 happenedWhat to do
PROJECT_CONFIG_REQUIREDA 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 initAdd --force or choose another --path
Warnings about unresolved references in a theme or component setThe 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 fileRead 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 heldAnother 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 changedRun 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 runA script rewrites the generated files after sync. Run it before sync or write elsewhere
SCSS: Undefined variableTheme 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 ModeSet Code Syntax on the tokens or codeSyntax on the output; check prefix and suffix
CSS keeps var(--...) but you need valuesSet resolveAliases: true on the output