Exporting tokens
One-click export of variables and styles as W3C DTCG JSON, and what the export contains.
On this page
The plugin exports every local variable collection and style in a Figma file as W3C DTCG JSON, bundled into one ZIP with a manifest.json.
Single-click exportLink to this section
Click Export in the plugin UI. The plugin runs five stages, posting progress messages back to the UI iframe:
- Export variables. Walks every local variable collection and every mode within each collection. Each variable's value (or alias) is written as a DTCG
$valueblock, with full Figma metadata in$extensions(variable id, scopes, alias data, hidden-from-publishing, type override forSTRING). One JSON file per(collection, mode)pair, named<collection>.<mode>.tokens.json. Collection names with leading underscores (_config,_calc) drop the underscore in the filename but preserve it undermanifest.collections.{key}.originalName. - Export text styles. Local text styles →
text.styles.tokens.json. Each style becomes a DTCG composite token (fontFamily,fontWeight,fontSize,lineHeight,letterSpacing). - Export effect styles. Local effect styles →
effect.styles.tokens.json. Drop shadows, inner shadows, and layer blurs become DTCGshadowtokens. - Export grid styles. Local layout grid styles →
grid.styles.tokens.json. - Generate manifest + ZIP. Builds
manifest.jsonwithname,generator: { name, version, exportedAt }, andcollections/stylesmaps. Bundles every token file plus the manifest into a single ZIP, then posts the ZIP back to the UI as a download.
The user clicks "Save" on the ZIP, unzips into <project>/src/tokens/ (overwriting the previous export), and runs:
pnpm systhema-core sync…to regenerate the project's CSS variables, types, and safelist. (Use systhema sync from the global CLI to also refresh agent-facing TOON references.)
The export is deterministic on Figma's variable order: variables are walked via collection.variableIds (Figma's UI ordering), and within each token file leaves are sorted before groups via sortLeavesFirst() to match Figma's native export shape. Re-exporting an unchanged design produces an identical ZIP — only the manifest.exportedAt timestamp differs.
What the export containsLink to this section
A typical export produces these files inside the ZIP:
Variable collections (one file per collection-mode pair):
colorPrimitives.value.tokens.json— raw colour scales (primitives.gray.50…primitives.gray.950, brand scales).colorSystem.default.tokens.json,colorSystem.dark.tokens.json, plus any custom themes — semantic colour mapping per theme.responsiveSizing.sm.tokens.json,responsiveSizing.md.tokens.json,responsiveSizing.lg.tokens.json— typography sizes, container widths, grid configs, paddings per breakpoint.font.value.tokens.json— font families, weights, fallback chains.tailwindcss.value.tokens.json— if the Figma file ships Tailwind extensions (rare; usually project-side).config._config.tokens.jsonandcalc._calc.tokens.json— plugin-internal helper values.- Any motion collection the file carries (e.g.
easings.value.tokens.json,transition.value.tokens.json) — Figma'sEasingandTimingvariables, exported aseasinganddurationtokens (see below).
Style collections (one file per style type):
text.styles.tokens.json—.text-h1….text-h6,.text-body,.text-lead,.text-small,.text-labeldefinitions.effect.styles.tokens.json— named shadow / blur effects.grid.styles.tokens.json— named grid configurations.
manifest.json — the index:
{
"name": "Systhema Figma Tokens",
"generator": {
"name": "@systhemaui/figma",
"version": "1.5.0",
"exportedAt": "2026-04-25T11:05:24+02:00"
},
"collections": {
"colorSystem": {
"modes": {
"default": ["colorSystem.default.tokens.json"],
"dark": ["colorSystem.dark.tokens.json"]
}
},
"config": {
"modes": { "_config": ["config._config.tokens.json"] },
"originalName": "_config"
}
},
"styles": {
"text": ["text.styles.tokens.json"],
"effect": ["effect.styles.tokens.json"],
"grid": ["grid.styles.tokens.json"]
}
}The collections.{key}.originalName field preserves Figma's collection name when it's been renamed for the filename (e.g. underscore prefix stripped).
Font families carry a CSS fallback stackLink to this section
Figma stores a font family as a single bare name — Geist, Switzer — because that's what it needs to resolve a real font face. CSS needs more than that: webfonts load with font-display: swap, so a bare font-family: Switzer paints once in the browser's built-in serif before the face arrives. That's the Times New Roman flash on a cold load.
So the export appends a system-sans fallback stack to every font-family value:
// font.value.tokens.json — what Figma stores: "Switzer"
"fontFamily": {
"$type": "string",
"$value": "Switzer, system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif"
}This applies to font-family variables (anything scoped FONT_FAMILY, or named …/fontFamily) and to the fontFamily a text style ships in its $value. Values that already contain a comma are treated as an author-provided stack and left exactly as written, as are DTCG references like {font.default.heading.fontFamily} and bare generic families like monospace. A family name that wouldn't be valid unquoted CSS (Font 2, Röck+Roll) is quoted.
On import the stack is stripped back to the first family before the value reaches Figma, so importing Geist, system-ui, … behaves exactly like importing Geist. The Figma variable only ever holds the bare name, which makes export → import → export idempotent — re-exporting appends the stack once, never twice.
@systhemaui/core reads the first entry as the family key, so font-geist and the rest of the Tailwind bridge resolve unchanged.
See Fonts for how core consumes the stack.
Colors with opacityLink to this section
Figma's "control opacity at scale" (September 2026) composes a colour variable from an aliased colour plus a separate opacity argument. Its native export writes the composition beside a FLATTENED $value:
"text-muted": {
"$type": "color",
"$value": { "colorSpace": "srgb", "components": [0, 0, 0], "alpha": 0.25, "hex": "#000000" },
"$extensions": {
"com.figma.composedColor": {
"colorArg": { "type": "alias", "alias": { "targetVariableName": "foundations/text" } },
"opacityArg": { "type": "alias", "alias": { "targetVariableName": "foundations/mutedOpacity" } }
}
}
}opacityArg is either a literal percentage ({ "type": "number", "value": 50 }) or an alias to a number token holding one. A same-collection colorArg carries only targetVariableName; a cross-collection one carries the full alias data, like com.figma.aliasData elsewhere. A composed token has no top-level com.figma.aliasData.
The export writes that block itself. @figma/plugin-typings (1.135) documents no composition — VariableAlias is still { type, id } — but the runtime returns one as an expression:
// variable.valuesByMode[modeId]
{
"type": "VARIABLE_EXPRESSION",
"expressionFunction": "COMPOSE_COLOR",
"expressionArguments": [{ "type": "VARIABLE_ALIAS", "id": "VariableID:4012:988" }, 25]
}The exporter converts it to the format above: the colour argument becomes colorArg (a same-collection target by name, a cross-collection one with its full alias data), the opacity percentage becomes opacityArg, and $value is the colour resolved in the mode being exported with the opacity applied. A literal RGBA colour argument works the same way, as colorArg: { "type": "color", … }.
On import the composition is written back as the same expression, in a pass after the alias passes so both argument variables exist. Whether setValueForMode accepts an expression is not something the typings answer, so the write is attempted and its outcome reported: on success the token counts under Composed colors set on the result screen, and on a refusal the importer falls back to the old behaviour — an existing variable keeps its value (writing the flat colour would detach the designer's alias and freeze the opacity), a variable this run created takes the flattened colour and is reported as created detached. Either way one warning per token names the reason the API gave:
foundations/text-muted: composed color (alias + opacity) cannot be written through the Plugin API yet (<message>); existing value keptThose lines appear under Warnings on the import result screen, next to the Errors list.
com.figma.rawValue is the probe for everything else. The exporter reports any value shape it does not recognise instead of guessing quietly: a value that is neither an RGBA, nor a VariableAlias, nor a documented easing, nor a COMPOSE_COLOR expression is copied verbatim into $extensions["com.figma.rawValue"] (with a best-effort $value), and so is a VariableAlias carrying any key beyond type and id, and a composed colour whose arguments could not be resolved. Each one also logs a console.warn naming the variable and the object's keys.
Color tokens shows the CSS core emits for a composed color.
Easing and timing variablesLink to this section
Figma's Easing and Timing variable types export in the same shape as Figma's own variable export, so the two can be mixed freely:
"easeOutCubic": {
"$type": "easing",
"$value": "cubic-bezier(0.33, 1, 0.68, 1)"
},
"defaultTransitionSpeed": {
"$type": "duration",
"$value": { "value": 150, "unit": "ms" }
}A named preset (Ease in, Ease out back, …) exports as the curve Figma's editor shows for it, and a custom bezier as its four points. Figma stores timing in seconds; the token carries milliseconds. The VALUE decides the type: a variable holding an easing exports as $type: "easing" even when Figma reports its resolved type as STRING, which is what older files return. Aliases behave like every other type: a same-collection alias is a {reference}, a cross-collection one is the resolved value plus com.figma.aliasData.
A spring (Gentle, Quick, Bouncy, Slow, custom) or a HOLD has no CSS curve. Its $value is a keyword fallback (ease-out, or step-end for hold) and the real easing rides in $extensions["com.figma.easing"], which the importer prefers over the string.
On import a cubic-bezier() matching a preset becomes that preset again (so export → import → export is stable), any other curve becomes a custom bezier, and the CSS keywords linear, ease, ease-in, ease-out, ease-in-out map to their Figma counterpart. A duration accepts the DTCG object in ms or s, a CSS string (150ms, 0.15s), or a bare number read as milliseconds. steps()/linear() easings and unit-less or unknown-unit durations are reported as import errors for that variable.
@systhemaui/core reads both collections and emits them as --ease-*, --duration-* and --transition-* custom properties; see Motion tokens.
What it does not containLink to this section
The plugin captures Figma's design data — variables and styles. It does NOT capture:
- Components. No frame-to-React-component generation. Agents pick Systhema components based on a frame's structural intent.
- Auto-layout / constraints. Figma's layout primitives don't map 1:1 to Systhema's
<Section>/<Columns>/<Stack>. The translation is intent-driven, not pixel-driven. - Project-specific config. Things that live in
<project>/systhema.config.ts:manifestoverrides (subset of what Figma exports).customTokens(project-only overrides; don't round-trip back).packages.{react|next|payload}settings.- Project-only
tailwindcssextensions.
- Figma plugin metadata beyond the
$extensionsblock — variable comments, style descriptions, and version history don't survive the export. - Asset files. Images, illustrations, and icons referenced inside Figma frames don't ship with the token export. They're handled separately (placed under
<project>/public/).