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

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

Symptom

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

{
  "$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

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

Symptom

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

Failing export

{
  "$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

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

Symptom

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

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

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

Symptom

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

Failing export

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

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

Symptom

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

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

Corrected config

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

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

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

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.