Appearance
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.fixedEnabledmeta 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/metacollapses into the singlecolorcollection — both are multi-mode and structurally identical, so the user (rightly) expects onecolorcollection rather thancolorplus a sibling holding a single variable. Balanced and Granular keep the split because their layout already groups by phase. The single-modemetacollection is unaffected — it can't be merged intocolorbecause their mode shapes differ.
Meta contents (always emitted, even on a brand-new empty theme with no colour-bearing entities):
- theme-level —
theme-name, optionaldescription - 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-steponly when the entity has alevelConfig;directiononly when the entity overrides it;parentsonly for frame / top-layer;calculate-ononly 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 oncontrast-step × level-count) - color-category meta —
seed-colorincolor/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 theon-{parent}/…nesting prefix and thec-Ntail (on-surfaces/surfaces-0/enabled/…). - interactionName -- present only when the condition is
interactive: trueAND the entity has at least one enabled interaction assigned (e.g.hover,focus,press). The unfactored baseline row (no interaction multipliers) omits this segment.idleis not an interaction — it's the implicit no-interaction baseline;disabledlives on the Conditions axis. For static conditions likeDisabledthe 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.
| Scenario | Path |
|---|---|
| Surface level 0 | enabled/surfaces-0/brand |
| Container base (no interaction) | enabled/surfaces-0/containers-0/brand |
| Container with interaction | enabled/surfaces-0/containers-0/hover/brand |
| Inverse container | enabled/surfaces-0/inv-containers-0/brand |
| Content on surface, base | on-surfaces/surfaces-0/enabled/c-0/brand |
| Content on surface, interaction | on-surfaces/surfaces-0/enabled/hover/c-0/brand |
| Inverse content on surface | on-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-container | on-containers/surfaces-0/containers-0/on-inv/enabled/c-0/brand |
| Inv-content on inv-container | on-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:
| Collection | Variable path | Light | Dark |
|---|---|---|---|
color/surfaces | enabled/surfaces-0/brand | near-white tone | near-black tone |
color/surfaces | enabled/surfaces-1/brand | slightly darker | slightly lighter |
color/containers | enabled/surfaces-2/containers-0/brand | base tone | base tone |
color/containers | enabled/surfaces-2/containers-0/hover/brand | hover tone | hover tone |
color/containers | enabled/surfaces-2/inv-containers-0/brand | inverse tone | inverse tone |
color/content-on-surfaces | on-surfaces/surfaces-0/enabled/c-0/brand | dark text color | light text color |
color/content-on-surfaces | on-surfaces/surfaces-0/inv/enabled/c-0/brand | light text color | dark text color |
Deduplication
Two levels of dedup reduce variable count:
- 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.
- 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.