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 the Push Vars page 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/), which keeps the collection count low.

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)
color/fixed/{entityName}                         -- single-mode Fixed collections (Value-only, like meta), frame + top-layer; only when settings.fixedEnabled

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/skip-duplicate-tokens, 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 | user-name | parents | figma-scopes | calculate-on | interactions | conditions. Several fields are conditional (e.g. pattern / contrast-step only when the entity has a levelConfig; direction only when the entity overrides it; parents only for frame / top-layer; calculate-on only for frame / top-layer). 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.

Collections carrying a {sanitized_theme_name}/color/... prefix are silently renamed to the flat layout on the next push — variable ids and node bindings survive the rename, so the migration is transparent.

Variable path structure

Within each collection, variables follow a path built from the two-axis state model (Conditions × Interactions) plus the entity's level / nesting context. The exact shape differs by entity type.

Frame / top-layer entities (containers / content) carry a contrast-index tail. The condition segment is always emitted — even for the lone seeded Enabled condition (the engine never omits it, so adding a second condition later doesn't rename every existing variable):

text
…/{conditionName}/{interactionName?}/c-{contrastIndex}/{colorName}
  • conditionName -- always present (e.g. enabled, disabled, selected). For containers it sits at the front of the nested prefix (enabled/surfaces-0/containers-0/…); for content it slots between the on-{parent}/… nesting prefix and the c-N tail (on-surfaces/surfaces-0/enabled/…).
  • interactionName -- present only when the condition is interactive: true AND the entity has at least one enabled interaction assigned (e.g. hover, focus, press). The unfactored baseline row (no interaction multipliers) omits this segment. idle is not an interaction — it's the implicit no-interaction baseline; disabled lives on the Conditions axis. 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, …). Present on content; containers use a level-index segment (containers-0, containers-1) instead.
  • colorName -- sanitized color name

Surfaces (level entities) are level-indexed and do not iterate interactions, but they do iterate conditions — the condition segment is emitted at the front, just like everywhere else:

text
{conditionName}/{levelIndex}/{colorName}

e.g. enabled/surfaces-0/brand for a surface.

Scrims (overlay entities) route through the derived-token generator, which puts the level prefix first and always emits a c-N contrast segment. The condition segment sits after the level prefix:

text
{levelIndex}/{conditionName}/c-{contrastIndex}/{colorName}

e.g. scrims-0/enabled/c-0/brand for a scrim.

Base (interaction) tokens

Within each condition, an unfactored base row (factors 1×1, no interaction multipliers) is always emitted alongside the per-interaction rows. It omits the interaction segment but keeps the condition segment:

text
…/{conditionName}/c-{N}/{colorName}

Base rows appear under the "Base" toggle in the preview. (There is no condition-free flat layer — base rows live inside each condition the entity opts into.)

Path examples

Paths below assume the stock entity names and the seeded Enabled (interactive) condition. Container / content nesting prefixes are shown in full.

ScenarioPath
Surface level 0enabled/surfaces-0/brand
Container base (no interaction)enabled/surfaces-0/containers-0/brand
Container with interactionenabled/surfaces-0/containers-0/hover/brand
Inverse containerenabled/surfaces-0/inv-containers-0/brand
Content on surface, baseon-surfaces/surfaces-0/enabled/c-0/brand
Content on surface, interactionon-surfaces/surfaces-0/enabled/hover/c-0/brand
Inverse content on surfaceon-surfaces/surfaces-0/inv/enabled/c-0/brand
Static condition (Disabled, no interaction segment)on-surfaces/surfaces-0/disabled/c-0/brand
Content on inv-containeron-containers/surfaces-0/containers-0/on-inv/enabled/c-0/brand
Inv-content on inv-containeron-containers/surfaces-0/containers-0/inv-on-inv/enabled/c-0/brand

Path segment ordering is structure-dependent: in granular the nesting context lives in the collection name and the per-variable path is shorter (e.g. content prefixes collapse to just inv / on-inv / inv-on-inv). The examples above show the balanced / compact merged-prefix form.

Example

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

CollectionVariable pathLightDark
color/surfacesenabled/surfaces-0/brandnear-white tonenear-black tone
color/surfacesenabled/surfaces-1/brandslightly darkerslightly lighter
color/containersenabled/surfaces-2/containers-0/brandbase tonebase tone
color/containersenabled/surfaces-2/containers-0/hover/brandhover tonehover tone
color/containersenabled/surfaces-2/inv-containers-0/brandinverse toneinverse tone
color/content-on-surfaceson-surfaces/surfaces-0/enabled/c-0/branddark text colorlight text color
color/content-on-surfaceson-surfaces/surfaces-0/inv/enabled/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.