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-level — theme-name, optional description
  • color-category root — color/contrast-model, color/collection-structure, color/skip-duplicate-tokens, color/palette/count, color/modes/count, color/interactions/count, color/conditions/count
  • per-mode — color/modes/{name}/seed-color | direction | contrast-factor | chroma-factor
  • per-entity — color/{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 meta — seed-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.