Resolving Figma Tokens Plugin Export Conflicts

Part of Design-to-Code Sync Workflows. This page is a troubleshooting guide for the Tokens Studio (formerly Figma Tokens) plugin export pipeline — diagnosing and resolving the breakages that silently corrupt your token JSON before Style Dictionary ever runs.

Export → Resolve → Build conflict flow Sequence showing the Tokens Studio export stage, the JSON resolve stage where conflicts are flagged, and the Style Dictionary build stage that produces CSS output. Tokens Studio plugin export JSON resolve alias + set merge Style Dictionary build → CSS output conflict point Common conflict sources set ordering / enabled sets $themes vs token-set mismatch unresolved {alias} references math/dimension parse errors JSON merge conflicts
The JSON resolve stage is where Tokens Studio export conflicts surface — set ordering, alias chains, and theme mismatches all fail here before Style Dictionary runs.

Prerequisites Link to this section

Before applying any fix, confirm your environment matches these baselines:

  • Tokens Studio for Figma (Figma Tokens plugin) v2.x or v3.x with the “Export to file/folder” or “Sync to GitHub/GitLab” storage provider configured.
  • Style Dictionary v3.x (for legacy config) or v4.x (DTCG-native). Fixes in this guide call out which version each applies to.
  • Token JSON stored in a Git repository with CI (GitHub Actions, GitLab CI, or similar). The pipeline must run style-dictionary build as a discrete step.
  • A local checkout where you can run style-dictionary build --verbose to reproduce failures without waiting for CI.
  • The $metadata and $themes keys exported from the plugin (enable “Include metadata” in plugin settings before each export).

Fix 1 — Token Set Ordering Conflicts Link to this section

Plugin set order compared with build glob order The ordered set list inside the plugin beside the build configuration, where an alphabetical glob reverses the intended precedence so the brand set no longer wins. Set order decides the winner — and the plugin order is not the build order In the plugin 1. core 2. semantic 3. brand — wins In the build config source: ["tokens/**/*.json"] glob order is alphabetical brand loses to semantic List the sets explicitly, in the same order the plugin uses. A glob is convenient exactly until a set is renamed and the precedence silently inverts.
The plugin order is metadata, not build order. Enumerating sources explicitly is the only way the two stay in agreement.

Symptom Link to this section

Style Dictionary outputs a stale or wrong value for a semantic alias — for example, --color-bg-surface resolves to the primitive blue instead of the neutral gray expected from your semantic set.

Failing export Link to this section

{
  "$metadata": {
    "tokenSetOrder": ["primitive", "brand", "semantic"]
  }
}

The semantic set references {primitive.color.neutral.100}, but a designer added a brand set that also defines primitive.color.neutral.100 with a different value. Because brand appears before semantic, the shadow wins.

Corrected config Link to this section

Enforce explicit set ordering inside your transform script, not just in the plugin export:

// scripts/merge-token-sets.mjs
import fs from 'fs';

const REQUIRED_ORDER = ['primitive', 'semantic', 'brand', 'component'];

const metadata = JSON.parse(fs.readFileSync('tokens/$metadata.json', 'utf8'));
const actual = metadata.tokenSetOrder ?? [];

for (const required of REQUIRED_ORDER) {
  if (!actual.includes(required)) continue;
  const idx = actual.indexOf(required);
  const expectedIdx = REQUIRED_ORDER.indexOf(required);
  if (idx !== expectedIdx) {
    console.error(`[token-merge] Set "${required}" is at position ${idx}, expected ${expectedIdx}`);
    process.exit(1);
  }
}
console.log('[token-merge] Set order valid:', actual.join(' → '));

Why this works. The plugin serializes $metadata.tokenSetOrder based on the panel drag-order at export time, which drifts whenever a designer adds a new set. The script enforces your canonical merge order in CI and fails fast before Style Dictionary misreads precedence.


Fix 2 — $themes vs Token-Set Mismatches Link to this section

Symptom Link to this section

Running style-dictionary build produces Error: Unknown token set "brand/dark" even though brand/dark.json exists in the repository.

Failing export Link to this section

{
  "$themes": [
    {
      "id": "dark-mode",
      "name": "Dark Mode",
      "selectedTokenSets": {
        "brand/dark": "enabled",
        "semantic": "enabled"
      }
    }
  ]
}

The plugin exported the theme referencing brand/dark, but the file on disk is named tokens/brand-dark.json (hyphen, not slash) because a CI step flattened the folder structure.

Corrected config Link to this section

Keep the plugin’s slash-separated namespace as a real folder hierarchy, and configure Style Dictionary to resolve paths consistently:

// sd.config.mjs
import { fileURLToPath } from 'url';
import { resolve, dirname } from 'path';

const __dirname = dirname(fileURLToPath(import.meta.url));

export default {
  source: ['tokens/**/*.json'],
  // Map each $themes entry to a Style Dictionary platform
  platforms: {
    css: {
      transformGroup: 'css',
      buildPath: 'dist/tokens/',
      files: [{
        destination: 'variables.css',
        format: 'css/variables',
        filter: token => token.attributes?.category !== 'primitive',
        options: { outputReferences: true }
      }]
    }
  }
};

In your token folder, preserve the slash as a real directory separator:

tokens/
  primitive/
    color.json
  brand/
    light.json
    dark.json        ← must match "brand/dark" in $themes
  semantic.json
  $themes.json
  $metadata.json

Why this works. Tokens Studio encodes set names as group/subgroup where the slash is a path separator. Flattening to group-subgroup.json breaks the $themes reference map. Preserving the directory structure means the plugin’s exported paths resolve 1:1 at build time.


Fix 3 — Unresolved {alias} References Link to this section

How an alias resolves, and the three ways it fails An alias reference resolving through a lookup into a concrete value, above three failure rows covering a missing set, a cross-brand reference and an upstream rename. {color.brand.500} reference as authored resolver looks up the path #2563eb resolved value Failure 1 — the referenced set was not included in the export: the path exists in the plugin, not in the file Failure 2 — the reference crosses a brand boundary: brand A points at a token only brand B defines Failure 3 — the path was renamed upstream: the alias still parses, so nothing errors until the CSS is inspected
All three failures produce the same symptom — a literal brace string in the compiled CSS. Distinguishing them requires checking which sets the export actually contained.

Symptom Link to this section

Style Dictionary logs Error: Reference Errors: Reference not found: {color.neutral.100} and the build exits non-zero. Generated CSS contains literal {color.neutral.100} strings instead of computed values.

Failing export Link to this section

{
  "color": {
    "bg": {
      "surface": { "value": "{color.neutral.100}", "$type": "color" }
    }
  }
}

The token color.neutral.100 exists only inside the primitive token set, but that set is not included in the active $themes entry for the current build.

Corrected config Link to this section

Add a pre-build step that validates every alias resolves within the merged source files:

// scripts/validate-aliases.mjs
import fs from 'fs';
import path from 'path';
import { globSync } from 'glob';

const files = globSync('tokens/**/*.json');
const merged = {};

for (const f of files) {
  const data = JSON.parse(fs.readFileSync(f, 'utf8'));
  // Skip metadata and theme files
  if ('$themes' in data || '$metadata' in data) continue;
  Object.assign(merged, data);
}

function resolvePath(obj, parts) {
  return parts.reduce((acc, key) => acc?.[key], obj);
}

const ALIAS_RE = /\{([^}]+)\}/g;
let errors = 0;

function checkValues(obj, location) {
  for (const [key, val] of Object.entries(obj)) {
    if (typeof val === 'object' && val !== null) {
      if ('value' in val || '$value' in val) {
        const rawVal = val.$value ?? val.value;
        for (const [, ref] of String(rawVal).matchAll(ALIAS_RE)) {
          const parts = ref.split('.');
          if (!resolvePath(merged, parts)) {
            console.error(`[alias-check] Unresolved: {${ref}} at ${location}.${key}`);
            errors++;
          }
        }
      } else {
        checkValues(val, `${location}.${key}`);
      }
    }
  }
}

checkValues(merged, 'root');
if (errors > 0) { console.error(`[alias-check] ${errors} unresolved alias(es)`); process.exit(1); }
console.log('[alias-check] All aliases resolve.');

Why this works. The plugin only validates aliases within the currently enabled sets in the UI. A set that is enabled in the plugin panel but disabled in the exported $themes entry will omit the source tokens that aliases depend on, causing downstream build failures. Checking all aliases against the merged set catches the gap before Style Dictionary attempts compilation.


Fix 4 — Math and Dimension Expressions Style Dictionary Cannot Parse Link to this section

Symptom Link to this section

Style Dictionary v3 logs [error] Could not convert math expression or silently outputs NaN in CSS for spacing and size tokens.

Failing export Link to this section

{
  "spacing": {
    "md": { "value": "{base.unit} * 4", "$type": "dimension" }
  }
}

Tokens Studio evaluates math expressions in the plugin UI, but exports the raw expression string when “Resolve math” is disabled or when using sync-to-git storage.

Corrected config Link to this section

Install the @tokens-studio/sd-transforms package and register its ts/resolveMath transform in your Style Dictionary config:

// sd.config.mjs
import StyleDictionary from 'style-dictionary';
import { register, permutateThemes } from '@tokens-studio/sd-transforms';

// Registers ts/resolveMath, ts/typography/css/shorthand, etc.
register(StyleDictionary);

export default {
  source: ['tokens/**/*.json'],
  preprocessors: ['tokens-studio'],
  platforms: {
    css: {
      transformGroup: 'tokens-studio',   // includes ts/resolveMath
      buildPath: 'dist/tokens/',
      files: [{
        destination: 'variables.css',
        format: 'css/variables',
        options: { outputReferences: false }
      }]
    }
  }
};

If you need to stay on vanilla Style Dictionary without the Tokens Studio transform package, pre-evaluate math before the build:

// scripts/resolve-math.mjs
import fs from 'fs';
import { evaluate } from 'mathjs';
import { globSync } from 'glob';

const MATH_RE = /^[\d\s\+\-\*\/\.\(\)]+$/;

function walkResolve(obj) {
  for (const [key, val] of Object.entries(obj)) {
    if (typeof val === 'object' && val !== null) {
      if (typeof (val.value ?? val.$value) === 'string') {
        const raw = val.$value ?? val.value;
        if (MATH_RE.test(raw.replace(/\{[^}]+\}/g, '1'))) {
          const resolved = evaluate(raw.replace(/\{[^}]+\}/g, '1'));
          if (val.$value !== undefined) val.$value = resolved;
          else val.value = resolved;
        }
      } else {
        walkResolve(val);
      }
    }
  }
}

for (const f of globSync('tokens/**/*.json')) {
  const raw = fs.readFileSync(f, 'utf8');
  if (!raw.includes('*') && !raw.includes('/')) continue;
  const data = JSON.parse(raw);
  walkResolve(data);
  fs.writeFileSync(f, JSON.stringify(data, null, 2));
  console.log(`[resolve-math] Processed: ${f}`);
}

Why this works. Style Dictionary’s built-in transforms do not evaluate arithmetic strings — they treat "{base.unit} * 4" as a reference to a token named base.unit} * 4, which does not exist. The ts/resolveMath transform (or the pre-evaluation script) reduces math expressions to bare numeric values before the reference-resolution pass runs.


Fix 5 — Merge Conflicts in Exported JSON from Multiple Designers Link to this section

Symptom Link to this section

After two designers push concurrent changes via the plugin’s GitHub sync, the token JSON files contain raw Git conflict markers (<<<<<<<, =======, >>>>>>>), and the pipeline fails with JSON.parse: unexpected token '<'.

Failing export Link to this section

<<<<<<< HEAD
{ "color": { "brand": { "primary": { "value": "#005FCC", "$type": "color" } } } }
=======
{ "color": { "brand": { "primary": { "value": "#0055FF", "$type": "color" } } } }
>>>>>>> feature/rebrand

Corrected config Link to this section

Add a pre-parse guard in CI that detects conflict markers and fails with an actionable message:

#!/usr/bin/env bash
# scripts/check-conflicts.sh
set -euo pipefail

CONFLICTS=$(grep -rl "^<<<<<<< " tokens/ || true)
if [ -n "$CONFLICTS" ]; then
  echo "[conflict-check] Unresolved merge conflicts in:"
  echo "$CONFLICTS"
  echo "Resolve conflicts in Figma by designating one designer as the merge owner."
  exit 1
fi
echo "[conflict-check] No conflict markers found."

For a structural merge (when both changes should survive), use a deep-merge tool instead of Git’s text merge:

// scripts/deep-merge-tokens.mjs
import fs from 'fs';
import { mergeWith, isObject } from 'lodash-es';

// Load both branches' versions (passed as CLI args)
const [, , base, incoming] = process.argv;
const baseData = JSON.parse(fs.readFileSync(base, 'utf8'));
const incomingData = JSON.parse(fs.readFileSync(incoming, 'utf8'));

function tokenMerger(objVal, srcVal, key) {
  // Prefer the incoming value for terminal token nodes; recurse on groups
  if (isObject(objVal) && ('value' in objVal || '$value' in objVal)) {
    console.warn(`[deep-merge] Token collision on key "${key}" — using incoming value`);
    return srcVal;
  }
}

const merged = mergeWith(baseData, incomingData, tokenMerger);
fs.writeFileSync(base, JSON.stringify(merged, null, 2));
console.log('[deep-merge] Merged successfully.');

Pair this with a .gitattributes rule to route JSON files through the merge script:

tokens/**/*.json merge=token-merge
git config merge.token-merge.driver 'node scripts/deep-merge-tokens.mjs %O %B %A'

Why this works. JSON is not line-merge-safe when two designers edit the same token key. A structural deep merge treats each {"value": …} node as an atomic unit and applies a deterministic precedence rule (incoming wins, with a log warning), instead of corrupting the file with raw conflict markers.


Verification Link to this section

After applying any fix, run the following sequence locally before pushing to CI:

# 1. Check for conflict markers
bash scripts/check-conflicts.sh

# 2. Validate alias resolution
node scripts/validate-aliases.mjs

# 3. Full Style Dictionary build with verbose output
npx style-dictionary build --verbose 2>&1 | tee sd-build.log

# 4. Assert no error lines in the log
grep -i "error\|unresolved\|reference not found" sd-build.log && exit 1 || echo "Build clean."

# 5. Spot-check a generated variable
grep "\-\-color-bg-surface" dist/tokens/variables.css

A clean build produces no [error] or [warning] lines in the Style Dictionary output and every generated --custom-property contains a resolved value, not a {reference} string or NaN.


Troubleshooting Reference Link to this section

Symptom Likely Cause Fix
Error: Reference not found: {color.neutral.100} Primitive set disabled in active $themes entry Add "primitive": "source" to every theme in $themes.json; re-run alias validation script
Generated CSS contains literal {spacing.md} strings Style Dictionary ran without ts/resolveMath; raw alias not resolved Register @tokens-studio/sd-transforms and use transformGroup: 'tokens-studio'
Token has wrong resolved value — wrong color applied $metadata.tokenSetOrder drifted from canonical merge order Run set-order validation script in CI; reset order in plugin panel and re-export
SyntaxError: Unexpected token '<' in JSON parse Git conflict markers in exported JSON Run check-conflicts.sh as the first CI step; designate a merge owner per file
Style Dictionary build succeeds but --spacing-md outputs NaN Math expression {base.unit} * 4 not pre-evaluated Pre-process with resolve-math.mjs or enable the ts/resolveMath SD transform
Error: Unknown token set "brand/dark" File renamed to brand-dark.json (flattened), breaking $themes path reference Restore slash-separated directory hierarchy; do not flatten nested set folders
Typography shorthand token not expanded to individual CSS properties typography composite token exported as single string Register ts/typography/css/shorthand from @tokens-studio/sd-transforms

Migration Note — Moving to the DTCG Format and Themes API Link to this section

Tokens Studio v2 used proprietary value keys and a $themes.json side-file for multi-theme configuration. The W3C Design Tokens Community Group specification (DTCG) uses $value and $type on every token, and the Figma Variables API (available on Enterprise) exports natively in DTCG format without a plugin intermediary.

Migrating from the plugin’s legacy format to DTCG changes the token node shape:

// Legacy (Tokens Studio v2)
{
  "color": {
    "brand": {
      "primary": { "value": "#005FCC" }
    }
  }
}

// DTCG-compliant
{
  "color": {
    "brand": {
      "primary": { "$value": "#005FCC", "$type": "color" }
    }
  }
}

Enable the migration in three steps:

  1. In Tokens Studio plugin settings, toggle “Use $value/$type (W3C DTCG)” before the next export. Re-export all sets to Git.
  2. Update your Style Dictionary config to Style Dictionary v4 with log.verbosity: 'verbose' and preprocessors: ['tokens-studio']. SD v4 reads $value/$type natively.
  3. Run node scripts/validate-aliases.mjs after the first DTCG export — alias references do not change format ({color.brand.primary} still resolves the same path), but composite tokens (typography, shadow, border) change their internal key names and must be re-checked.

The $themes side-file remains the mechanism for configuring which sets are active in which theme even in DTCG mode. Keep your automating Figma-to-CSS variable sync pipeline updated to use the correct $themes schema for whichever format version you are on, since the theme resolution logic changed between plugin v2 and v3.

For multi-brand setups where different brand teams own separate token sets, see handling multi-brand export conflicts in token sync for a parallel-branch merge strategy that avoids the structural collisions described in Fix 5.