For docs: Variable Modes
A two-collection setup showing mode-keyed sync: a single-mode Primitives collection referenced by a Semantic collection with light and dark modes. Variable modes sync is free for everyone during the v1.3 preview.
tokens/
├── primitives.json # Single-mode collection: map as Variables, "First mode only"
├── semantic.json # Mode-keyed (light/dark): map as Variables, "All modes"
├── color-styles.json # Color styles: map as Color styles
└── text-styles.json # Text styles (Inter): map as Text styles
Pull primitives.json first so the alias targets exist when semantic.json is pulled.
| File | Target | Collection Name | Variable Modes |
|---|---|---|---|
tokens/primitives.json |
Variables | Primitives |
First mode only |
tokens/semantic.json |
Variables | Semantic |
All modes |
tokens/color-styles.json |
Color styles | n/a | n/a |
tokens/text-styles.json |
Text styles | n/a | n/a |
Requires a Figma plan that allows more than one mode per collection (Professional or higher). On the Starter plan the second mode is skipped and reported.
semantic.jsonroot keys are mode names (light,dark); each mode has an identical structure with different values.- Values like
{Primitives.gray.900}are alias references. After pulling, the Semantic variables are bound to the Primitives variables, and toggling the mode in Figma switches every resolved value. color.action.primarycarries a$descriptionand an$extensionsblock in both modes. The description lands on the Figma variable;$extensionsis left untouched on push (GitFig only owns$value,$type, and$description).space.insetshows a number alias across modes (same primitive in both, so the value is mode-independent while still living in the mode-keyed file).- Push after editing a
darkvalue in Figma: the diff touches one file and one mode.
Styles have no modes, so they sync as flat files next to the mode-keyed one. color-styles.json and text-styles.json are in the exact shape GitFig writes on push ("Brand-Primary": { "$value": "#3B82F6", "$type": "color" }; a text style's value carries fontFamily, fontWeight, fontSize, lineHeight, letterSpacing). Pull creates or updates styles by name; push writes them back in the same atomic commit as the variables. The text styles use Inter, which Figma ships, so they import without installing a font. A text style whose font is not available in the file is skipped with a warning. $extensions on a style survives a push the same way it does on a variable.
Use this to confirm your setup end to end.
- Pull all four files (
primitives.jsonbeforesemantic.json). Figma has a Primitives collection (12 variables, 1 mode) and a Semantic collection with exactly two modes,lightanddark, holding 8 variables each.Semantic/color/text/primaryshows as an alias toPrimitives/gray/900, andPrimitives/gray/900carries the descriptionDarkest gray, café-grade contrast ✓. The Styles panel shows three color styles (Brand-Primary,Brand-Primary-Hover,Overlay-Scrim) and three text styles (Heading-Large,Body,Caption). - Push with no edits. GitFig reports no changes, and
$extensionsis still present in both files on GitHub. - Change one
darkvalue in Figma and push. The Changes list shows a single change with adarkmode badge; the commit touches onlysemantic.json, only inside"dark". - Delete both collections in Figma and pull again. Both are recreated with their modes, aliases re-bound, and descriptions intact.
- Point a mapping set to First mode only at
semantic.json. GitFig refuses with the mode-keyed file guard and creates no variables; switch that mapping to All modes to import it.
Add a third root key to semantic.json on GitHub, for example "dim": {}, and pull. GitFig adds a dim mode to the Semantic collection; Figma seeds it from the default mode, and your next push fills in its values.