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 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/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/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 | 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 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.
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: trueAND the entity has interactions assigned (e.g.idle,hover,press). 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, …)
- 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
| Scenario | Path |
|---|---|
| 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, interactive | inv/enabled/idle/c-0/brand |
| Inverse, static | inv/disabled/c-0/brand |
| Content on inv-container | on-inv/enabled/idle/c-0/brand |
| Inv-content on inv-container | inv-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):
| Collection | Variable path | Light | Dark |
|---|---|---|---|
color/surfaces | 0/brand | near-white tone | near-black tone |
color/surfaces | 1/brand | slightly darker | slightly lighter |
color/containers/surfaces-2 | c-0/brand | base (1x1) | base (1x1) |
color/containers/surfaces-2 | idle/c-0/brand | medium tone | medium tone |
color/containers/surfaces-2 | inv/idle/c-0/brand | inverse tone | inverse tone |
color/content-on-surfaces/surfaces-0 | idle/c-0/brand | dark text color | light text color |
color/content-on-surfaces/surfaces-0 | inv/idle/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.