Token sync layer
Sync Figma design variables into code-ready token files via DTCG JSON and Style Dictionary, emitting framework-specific outputs (shadcn/Tailwind, MUI, vanilla CSS, iOS Swift, Android Kotlin, or custom), and set up the reusable "sync figma tokens" command. Use this when the user wants to turn Figma tokens into code, sync design tokens, generate tokens.css or a Tailwind config or tokens.swift, set up a token pipeline, or run "sync figma tokens". Also trigger when the user mentions Style Dictionary, design token export, or keeping code in sync with Figma.From its SKILL.md
npx -y skills add jrpease/throughline --skill token-sync-layerAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
SKILL.md
16.7 KB, as published. Nobody here has run it
Token sync layer
Compiles Figma variables into code. The pipeline is one-directional — Figma is the source of truth for token values; code consumes generated artifacts that are never hand-edited:
Figma variables → DTCG JSON → Style Dictionary → adapter output (per platform)
This skill both performs the sync and installs the reusable /sync-figma-tokens
command so the user can re-run it anytime tokens change.
Calibrate
Read user.codingLevel (${CLAUDE_PLUGIN_ROOT}/references/coding-level.md) and scale explanation
accordingly. The pipeline involves several developer concepts (JSON, build
tools, PRs) — for new users, explain each plainly the first time; for
comfortable users, be terse. Actions are identical across levels.
Prerequisites (offer to run them, don't bail)
Read the manifest. This skill needs two things:
- Tokens in Figma —
tokens.semanticBuiltshould be true. If no tokens exist, offer to runtoken-builderfirst. - At least a local git repo —
workspace.stagemust belocal-gitorgithub(so the sync can land changes as a reviewable diff/PR). If it's stillfolder, this is the soft-nudge seam: offer to runrepository-builderto add version history first. Explain plainly why ("the sync writes code files, and git lets you review exactly what changed before keeping it"). If the user is atlocal-gitbut notgithub, strongly recommend connecting a remote (GitHub or similar) so the sync can land as a real reviewable PR — the cleanest review model — but don't force it; a reviewed branch diff works atlocal-git.
A live Figma connection is also required — do a cheap liveness read; if it
fails, offer figma-environment-setup.
Step 1 — Choose target platform(s)
Ask which platform(s) the user is building for. Read
${CLAUDE_PLUGIN_ROOT}/references/sync-adapters.md for the two-tier adapter model.
- Curated (Tier 1):
shadcn,tailwind,mui,vanilla-css,ios-swift. Vetted presets — high confidence. - Generated (Tier 2): any other framework (Ant Design, Chakra, HeroUI, Android/Kotlin, Flutter, etc.). The skill generates an adapter and verifies it against a real component before trusting it.
Always tell the user which tier they're on. If they name a curated one, say
it'll be solid. If they name anything else, be honest: "That's not one I have a
vetted preset for — I'll generate an adapter from its conventions and we'll check
it against a real component before relying on it." Follow the Tier 2 protocol in
the reference, and offer to save a validated generated adapter to
packages/tokens/adapters/ (record it in sync.customAdapters) so future syncs
reuse it.
A user can target several platforms at once. Record the choices in
sync.platforms. The adapter is what absorbs all framework-specific shaping —
which is why the Figma variables stayed framework-neutral.
Also set project.uiFramework if it's still null (this or component-builder sets
it at first relevance, whichever runs first; the other reads it). If
component-builder already set it, the chosen adapter(s) should be consistent with
it.
Step 2 — Extract Figma variables to DTCG JSON
Using the active write mechanism (figma.mechanism, default Console MCP —
prefer its full-design-system extraction, which works on any Figma plan), read
every variable collection (there are now several per tier, e.g.
_Color/Primitive, Spacing/Primitive, Color/Semantic, …) and normalize them
into DTCG-format JSON ($value, $type, with semantic tokens expressed as
{group.token} references to primitives, not flattened literals).
Three rules for the multi-collection structure:
- Iterate N collections, not a fixed two. Don't assume one
Primitives+ oneSemantic. - Single-mode collections are non-themed — a collection with one mode (e.g.
Spacing/Semantic) emits flat values, never a phantomdefaulttheme alongside:root/.dark. - Primitives can be multi-mode —
_Color/Primitivecarries a Brand axis, so emit brand themes from the primitive tier, not just light/dark from semantics. Preserve modes (light/dark/brand/device) as the DTCG structure the adapters expect. - Name mapping: collapse the tier/category prefix into clean token names —
_Color/Primitive+gray/500→color.gray.500(notcolor.primitive.gray.500);Color/Semantic+text/primary→color.text.primary. The category drives the top-level group; the tier (and the_) is dropped from the emitted name. - Opacity: normalize 0–100 → 0–1 on extraction. Opacity tokens are stored in
Figma on the 0–100 scale (a quirk of how Figma binds variables to a node's
opacityfield — see the opacity scale rule intoken-builder). CSSopacity, Tailwind opacity, and native alpha are all 0–1, so when normalizing any opacity token (theOpacity/*collections, or any FLOAT bound to opacity) divide its value by 100 before writing DTCG (40→0.4). Do this once at extraction so every adapter emits a correct 0–1 value; an un-normalized40lands asopacity: 40in CSS and renders the element fully opaque. token-builder and token-sync-layer are a matched pair on this — neither is correct alone.
This DTCG JSON is the canonical intermediate. Write it to a known location in
packages/tokens/ (e.g. packages/tokens/dtcg/tokens.json). Do not trust the
MCP's built-in CSS/Tailwind exporters for final output — route through Style
Dictionary so the adapter system governs the result.
Brownfield transforms (learned the hard way)
On a retrofit (tokens.intakeMode: "retrofit"), apply these transforms in
addition to the opacity normalization above. Each cost real debugging time on a live
retrofit — see the guardrails in
${CLAUDE_PLUGIN_ROOT}/references/brownfield-retrofit.md.
- Alpha as channels, not baked CSS vars (so Tailwind
/opacitysurvives). Emit colors as space-separated channels with a slash-alpha slot —--color-x: 239 68 68;consumed asrgb(var(--color-x) / <alpha-value>)— rather than a finishedrgba(...). A bakedrgba()can't accept Tailwind's/opacitymodifier; the channel form keepsbg-x/50working after the retrofit. /opacityon var-based tokens →color-mixor channel alpha (guardrail 6). Where existing code applies a/opacitymodifier to a token that is now a CSS var, you can't fold the alpha into the var. Convert tocolor-mix(in srgb, var(--color-x) NN%, transparent)(or the channel-alpha form above). Never carry a raw/opacityonto a var-based token.- Round float32 noise at the export boundary (guardrail 2). Figma stores values as
float32 and re-quantizes on write, so normalizing inside Figma is a no-op. Round at
the export boundary instead —
Math.round(v * 100) / 100as values leave the pipeline — so0.30000001192092896lands as0.3in the generated files. Do this in the extraction/transform step, never by hand-editing the generated output (guardrail 7).
These are transforms on the values the adapters emit; the adapter presets themselves
are unchanged. See ${CLAUDE_PLUGIN_ROOT}/references/sync-adapters.md for where they
fit in the adapter output.
Step 3 — Set up Style Dictionary + adapters
Install and configure Style Dictionary v4 in packages/tokens/. For each chosen
platform, apply its adapter preset (per ${CLAUDE_PLUGIN_ROOT}/references/sync-adapters.md):
register the platform, transform group, format, and outputReferences
(true for web → preserves the semantic→primitive cascade; false for native →
flattens). Web adapters emit :root/.dark (or [data-theme]); the shadcn
adapter also emits a Tailwind preset.
Native targets import the shipped configuration; they do not transcribe it.
Copy ${CLAUDE_PLUGIN_ROOT}/scripts/lib/sd-native.mjs into
packages/tokens/scripts/lib/ (see Step 4) and call it. The stock ios-swift
and compose transform groups emit every px-authored dimension at ×16 its
value — valid, compiling, silently wrong — and mishandle color-mix() and
dual-node DTCG the same way. Never build a native platform from a stock
transformGroup.
import StyleDictionary from 'style-dictionary';
import { registerNativeTransforms, nativePlatform, nativeSources }
from './scripts/lib/sd-native.mjs';
registerNativeTransforms(StyleDictionary);
for (const mode of MODES) { // e.g. ['light', 'dark']
const sd = new StyleDictionary({
source: nativeSources(sourcesFor(mode)), // guards against a mode collapse
preprocessors: ['dtcg/resolve-dual-node'],
platforms: {
ios: nativePlatform({ platform: 'ios-swift', buildPath: `ios/${mode}/` }),
// android also requires packageName:
// android: nativePlatform({ platform: 'android-kotlin',
// buildPath: `android/${mode}/`, packageName: 'com.example.tokens' }),
},
});
await sd.buildAllPlatforms();
}
One build per mode combination, and never a glob. Style Dictionary dedupes
by dot-path, so a single build over the whole token directory collapses light
and dark into whichever file sorted last, silently dropping a mode. Passing each
mode's sources through nativeSources turns that into a thrown error naming the
colliding paths. The dtcg/resolve-dual-node preprocessor throws too, on its
own collision: a dual node's hoisted child renamed to a camel-joined name that
an existing sibling or an earlier hoist in the same pass already has. Then run
tokens:validate-output against each generated file with that same source
list. See ${CLAUDE_PLUGIN_ROOT}/references/native-adapter-config.md.
Execution model — subagent dispatch with model routing. Generating each
platform's output is independent and verifiable. If your host supports subagent
dispatch, dispatch one code-executor per adapter — each produces its
platform's files and verifies them (for web: the config builds, the expected
files appear, references resolve; for native: tokens:validate-output passes —
"the config builds" is not verification, it is the condition under which all
four known native failure modes ship silently) — then a reviewer
to check each before combining. Choose each subagent's model from its role tier
per ${CLAUDE_PLUGIN_ROOT}/references/agent-routing.md (code-executor → fast,
reviewer → balanced), and only dispatch once each adapter's spec is complete
enough to transcribe. If your host has no subagent dispatch, generate and verify
each adapter inline instead. For a single platform, run inline either way. This
is a code-gen stage, so these subagents may run in parallel — unlike Figma
authoring, which is always sequential.
Step 4 — Build and place outputs
Run the Style Dictionary build. Outputs land in packages/tokens/<platform>/
as build artifacts — regenerated every sync, never hand-edited. Wire
packages/tokens/package.json to export them so the UI package, Storybook, and
any future app consume them.
Install the native token toolkit — all four files, as a set. Copy
${CLAUDE_PLUGIN_ROOT}/scripts/validate-token-output.mjs into
packages/tokens/scripts/, and
${CLAUDE_PLUGIN_ROOT}/scripts/lib/dtcg.mjs,
${CLAUDE_PLUGIN_ROOT}/scripts/lib/native-literal.mjs, and
${CLAUDE_PLUGIN_ROOT}/scripts/lib/sd-native.mjs into
packages/tokens/scripts/lib/. sd-native.mjs and the validator both import
dtcg.mjs and native-literal.mjs, so copying any of them without the others
breaks at import. Then register the gate so it stays live on every future sync:
"tokens:validate-output": "node scripts/validate-token-output.mjs --min-match 1"
Invoke it once per native output file, passing the same --source list that
file's build used. --min-match 1 is what makes it a gate: the flag defaults to
0.5, so without it a 60% match rate exits 0.
Step 4.5 — Icon code sync (install check + custom SVGR)
Icons reach code differently from tokens, so handle them here if the system has
icons (icons.built true):
- Library icons (Lucide/Material) — verify the npm package is installed
(
icons.packageInstalled); install it if not. Then check version drift: compare the installed package version againsticons.version(the version the Figma mirror was built from). If they differ, an icon could exist on one side and not the other — flag it and offer to align them (bump the package, or note the Figma mirror should be refreshed). Never generate library icon component code — the package is the code. - Custom icons — these the repo owns, so generate them: export the custom
SVGs from Figma, optimize, and componentize via SVGR into
packages/ui(or a dedicated icons package). This is real code generation and rides the samecode-executor(fast) +reviewer(balanced) routing and PR-review as token output (SVGR transforms are the textbook mechanical op the fast tier is for) — see the Step 4 execution model.
Step 5 — Full regeneration + rename detection (the safety net)
The sync is a full regeneration every run: outputs are rebuilt from current Figma state, so a token deleted in Figma disappears from output (no orphans). But a naive full regen can't tell a rename from a delete-plus-add — and a rename silently breaks every consumer of the old name.
So before writing, diff against the previous output and run a rename
heuristic: if a token vanished and a new one appeared with an identical $value
and $type, flag it as a probable rename (e.g. color.bg.default → color.surface.default?). Surface these prominently. Also surface plain
deletions (consumers will break — that's intentional, but the human should see
it).
Step 6 — Land it as a reviewable PR (never silent)
Never overwrite token files silently. The output of a sync is a pull request
(or, if the repo is only local-git, a clearly-described diff/commit on a
branch the user reviews). The PR body is the human safety net — it should
summarize: tokens added, changed, deleted, and probable renames flagged for
review so the reviewer can do a find-and-replace instead of merging a break.
For new users, explain what a PR is plainly ("a proposed change you review and
approve before it becomes official — like track-changes for code") and walk the
review. For comfortable users, just open it.
Chromatic note. This PR is token-only — it changes packages/tokens but no
story files. Token changes are global, so Chromatic should re-snapshot every
story for a sync PR. Don't rely on TurboSnap (onlyChanged: true) here — its
incremental model keeps missing global token changes; default to full snapshots.
If a sync PR reports "0 snapshots captured," TurboSnap is the cause — see the
TurboSnap section in storybook-chromatic-builder.
Update the manifest: sync.lastRun (timestamp), tokens.lastSync, confirm
sync.platforms. Append token-sync-layer to completedSkills.
Step 7 — Install the /sync-figma-tokens command
Set up the reusable command (the plugin ships it in commands/) so the user can
re-run this whole pipeline anytime tokens change in Figma. Explain that the
workflow going forward is: tweak tokens in Figma → run /sync-figma-tokens →
review the PR → merge. That loop is the entire point of the one-directional
source-of-truth model. Confirm the command works by describing how to invoke it.
What this skill must NOT do
- Never hand-edit generated token files or let the user treat them as editable — they're build artifacts; changes go in Figma and re-sync.
- Never flatten semantic→primitive references for web adapters (kills theming).
- Never write token files silently — always a reviewable PR/diff.
- Never merge or push to a protected branch on the user's behalf without review.
- Never skip rename detection — a silent rename is the worst failure mode.
- Never bake alpha into finished
rgba(...)on a retrofit — emit channels (rgb(var(--x) / <alpha-value>)) so Tailwind/opacitymodifiers survive. - Never carry a
/opacitymodifier onto a var-based token — convert tocolor-mixor channel alpha (guardrail 6). - Never normalize float32 inside Figma (it re-quantizes) — round at the export
boundary (
Math.round(v*100)/100, guardrail 2).