Skip to content

Variable Structure

When you push variables, Systema creates Figma variable collections. Each collection contains variables with values per enabled mode.

Single-theme-in-Figma policy

A Figma file holds exactly one Systema theme at a time. The plugin still supports multiple themes internally (for experimentation), but collections are never per-theme-prefixed — there is no {themeBase}/color/... layout. Users who want several design directions in parallel should use separate Figma files as libraries. See Push Vars below for how the plugin enforces this on push.

Collection naming

Direct and inverse tokens are merged into the same collection (inverse tokens use path prefixes inv/, on-inv/, inv-on-inv/). This halves the number of collections compared to the previous separate-collection approach.

Collections use a flat, theme-agnostic layout. The top-level color segment is the theme category — it reserves namespace for future categories (type, size, layout) so that adding them later won't collide with existing color collections.

text
meta                                             -- theme-name (STRING) — theme-level, category-agnostic
color/meta                                       -- seed-color (COLOR) — color-category metadata
color/{entityName}                               -- surfaces, scrims
color/{containerName}/{surfaceName}-{N}          -- containers + inv-containers
color/{contentName}-on-{surfaceName}/{surfaceName}-{N}   -- content + inv-content on surfaces
color/{contentName}-on-{containerName}/{surfaceName}-{N} -- content on containers + inv-containers (all directions)

meta is a single-mode collection (one Value mode in Figma) that holds documentation about the theme itself — it reads the same regardless of light/dark/etc, so duplicating every mode there only cluttered the Variables panel. color/meta stays multi-mode because seed-color genuinely varies per mode; any future per-mode color-category field joins it.

In Compact structure color/meta collapses into the single color collection — both are multi-mode and structurally identical, so the user (rightly) expects one color collection rather than color plus a sibling holding a single variable. Balanced and Granular keep the split because their layout already groups by phase. The single-mode meta collection is unaffected — it can't be merged into color because their mode shapes differ.

Meta contents (always emitted, even on a brand-new empty theme with no colour-bearing entities):

  • theme-leveltheme-name, optional description
  • color-category rootcolor/contrast-model, color/collection-structure, color/palette/count, color/modes/count, color/interactions/count, color/conditions/count
  • per-modecolor/modes/{name}/seed-color | direction | contrast-factor | chroma-factor
  • per-entitycolor/{entityName}/contrast-ratios | level-count | pattern | contrast-step | direction | colors | preview-shape | parents | calculate-on | interactions | conditions. All multi-value fields are comma-separated strings (e.g. colors: "Pink Apotheosis, Blue-Action", contrast-ratios: "1, 2.1, 4.8, 8.2" — blank for stepped patterns where the ladder lives on contrast-step × level-count)
  • color-category metaseed-color in color/meta (per mode)

meta holds theme-level identity. Future categories get their own siblings (type/meta, layout/meta, …); the category-agnostic meta collection stays dedicated to theme-level identity.

Legacy tester files pushed by older builds carried a {sanitized_theme_name}/color/... prefix. On the next push those collections are silently renamed to the flat layout — variable ids and node bindings survive the rename, so the migration is transparent.

Variable path structure

Within each collection, variables follow this path. The two-axis state model (Conditions × Interactions) drives the segments:

text
{conditionName}/{interactionName?}/c-{contrastIndex}/{colorName}
  • conditionName -- always present on frame / top-layer entities (e.g. enabled, disabled, selected). Omitted on surfaces / scrims.
  • interactionName -- present only when the condition is interactive: true AND the entity has interactions assigned (e.g. idle, hover, press). For static conditions like Disabled the engine collapses to one row per (contrast, color) — no interaction segment.
  • c-{N} -- contrast level index (c-0, c-1, c-2, …)
  • colorName -- sanitized color name

Surfaces and scrims use a simplified path (neither condition nor interaction iterate on level / overlay entities):

text
{levelIndex}/{colorName}

Base tokens

Independent of the conditions × interactions matrix, base tokens are emitted as a flat reference layer with factors 1×1 (no state multipliers). Their path omits both the condition and interaction segments:

text
c-{N}/{colorName}

Use these when you don't need stateful tokens. They appear under the "Base" toggle in the preview.

Path examples

ScenarioPath
Base token (no state)c-0/brand
Interactive condition (Enabled + Idle)enabled/idle/c-0/brand
Static condition (Disabled, no interaction segment)disabled/c-0/brand
Inverse, interactiveinv/enabled/idle/c-0/brand
Inverse, staticinv/disabled/c-0/brand
Content on inv-containeron-inv/enabled/idle/c-0/brand
Inv-content on inv-containerinv-on-inv/enabled/idle/c-0/brand

Example

For a theme with 3 surface levels, 4 container contrasts, 2 modes (Light/Dark), 1 color (Brand):

CollectionVariable pathLightDark
color/surfaces0/brandnear-white tonenear-black tone
color/surfaces1/brandslightly darkerslightly lighter
color/containers/surfaces-2c-0/brandbase (1x1)base (1x1)
color/containers/surfaces-2idle/c-0/brandmedium tonemedium tone
color/containers/surfaces-2inv/idle/c-0/brandinverse toneinverse tone
color/content-on-surfaces/surfaces-0idle/c-0/branddark text colorlight text color
color/content-on-surfaces/surfaces-0inv/idle/c-0/brandlight text colordark text color

Deduplication

Two levels of dedup reduce variable count:

  1. Generation-time -- if all modes produce the same color for a token as the previous contrast level (clamped to 0 or 100), the token is skipped.
  2. Push-time -- FNV-1a hash comparison: if a collection's computed hash matches the last pushed hash, the entire collection is skipped during Figma push.

All rights reserved.