Move brand modes down to Semantic where designers and devs actually bind. Flatten Primitives into a clean, unmoded palette library. Keep Theme, Regulatory, and Component exactly where they are. One mode axis per collection, no magic, no drift.
→ Token Reference v4 → Compare with v3 (Option A) → Brand Audit
v4 is a proposal. v3.1 is the current shipped architecture — kept live on the landing page as Option A.
After v3.1 shipped, both devs and designers pushed back that brand modes sit on the wrong collection. They bind to Semantic. The brand switch is two collections away. The complaint is one sentence, from two angles:
"I bind a button to interactive/primary/bg — that's Semantic. I want to preview it on Snabbare. But the brand mode switch is on Primitives, a collection I don't normally touch. Why isn't it where I work?"
"I consume Semantic via Tailwind. My bundle is one flat CSS file. All nine brands run through it, so where's the brand variation? Nowhere — I'm hand-writing theme.css."
color/lyllo/primary-600, color/snabbare/primary-600, etc. One canonical location per hex.theme.css.Two collections change shape. Three stay identical to v3.1. The number of collections is unchanged.
color/<brand>/<ramp> · ~990 tokens
cta.* · header.* · menu-row.* · marketing-locked
★ = changed from v3.1. Brand is the mode axis on Semantic, not on Primitives. Primitives becomes a flat palette library. Every hex lives in exactly one place.
Five collections, two rows changed.
| Collection | v3.1 (Option A · shipped) | v4 (Option B · proposal) |
|---|---|---|
| 1 · Primitives | 20 brand modes · ~110 tokens each · ~2200 values | ★ unmoded · flat names · ~990 tokens |
| 2 · Theme | 2 modes (light/dark) · ~28 tokens | unchanged |
| 3 · Semantic | no modes · ~340 tokens, aliases Primitives + Theme | ★ 9–20 brand modes · ~340 tokens per mode |
| 4 · Regulatory | 7 jurisdiction modes · ~22 tokens | unchanged |
| 5 · Component | no modes · ~35 alias packs · locked | unchanged |
| Axis | v3.1 | v4 |
|---|---|---|
| Brand | Primitives | Semantic |
| Theme | Theme | Theme |
| Jurisdiction | Regulatory | Regulatory |
Each mode axis lives in exactly one collection. No cross-collection mode-matching. No "Primitives and Semantic both have a brand mode named Lyllo, please stay in sync." Just aliases that explicitly name their target.
Values flow right to left through the chain. A consumer binds to Component (or Semantic); Figma walks backward through the aliases to land on a hex in Primitives.
Reading: A → B means B aliases from A. Regulatory (4) sits beside Semantic as a narrow override layer for compliance tokens only.
Component aliases Semantic only. Never reaches around to Primitives directly. Never touches Theme directly. That's why Component doesn't need modes of its own — all brand variation is resolved by the time the alias lands in Semantic.
One button. One hex answer. Traced through every collection in v4.
# Frame: <button> fill binds to a Component token Component (no modes): cta/primary/bg → Semantic.interactive/primary/bg Semantic (mode = snabbare): interactive/primary/bg → Primitives.color/snabbare/primary-600 Primitives (unmoded): color/snabbare/primary-600 = #1e8f3e # Snabbare green # Frame renders: #1e8f3e
Switch Semantic's mode to lyllo — the Component bind stays identical. Semantic's alias, per its lyllo mode, now points at color/lyllo/primary-600 = #ff6699. The button becomes Lyllo pink. No magic: each mode has an explicit alias target.
At no point does Figma have to "match modes across collections by name." Semantic's snabbare mode just contains an explicit alias: "point at color/snabbare/primary-600 in Primitives." Primitives is unmoded, so there's nothing to match — it returns the one hex it holds.
interactive/primary/bg (Semantic).snabbare.dark.cta/primary/bg (Component) or interactive/primary/bg (Semantic).snabbare.dark.interactive/primary/bg, text/primary, cta/primary/bg, etc.).theme.css.dist/tokens/<brand>-<theme>.css per combo — 18 bundles for 9 brands × 2 themes.<html data-brand="snabbare" class="dark">.@theme block maps CSS vars to utility classes. Components keep using bg-interactive-primary./* Auto-generated · dist/tokens/snabbare-dark.css */ [data-brand="snabbare"] { --color-interactive-primary-bg: #1e8f3e; --color-text-primary: #0a0a0a; /* ... */ } [data-brand="snabbare"].dark { --color-interactive-primary-bg: #1e8f3e; --color-text-primary: #ffffff; }
Semantic is generic roles. Component is specific UI elements that can't be expressed by a generic role alone. Three real examples:
Snabbare's header is not brand primary. Using interactive/primary/bg paints the wrong colour. Component pins it:
header/bg → Semantic.chrome/header/bg → Primitives.color/snabbare/header-chrome = #0b5ed7
Designers wanted action icons and chevrons themed independently. Semantic's generic icon/action couldn't split. Component does:
menu-row/action-icon → Semantic.icon/action menu-row/chevron → Semantic.icon/chevron
CTA radius differs by context: form / card / wallet / landing. One Semantic role can't carry four radius values. Component does:
cta/primary/bg → Semantic.interactive/primary/bg cta/form/radius = 8 cta/wallet/radius = 4 cta/landing/radius = 999
Component tokens alias Semantic. When Semantic's brand mode changes, Semantic resolves to a different Primitive. Component's aliases to Semantic automatically resolve through that. Brand variation propagates through the chain without Component participating.
Four phases. Each one verifiable on its own. Global Search prototype at localhost:5180 is the validation surface throughout.
brand-audit.html.AcbohbUnh3brJw52d3YlY6.lyllo (default) + snabbare modes first.dist/tokens/.theme.css.<html data-brand> + .dark at localhost:5180.Because each axis owns exactly one collection, each kind of addition touches one place.
color/<newbrand>/* to Primitives (~110 new flat tokens).<newbrand>-light.css and <newbrand>-dark.css appear in dist/.color/<subbrand>/accent* to Primitives.high-contrast).Marketing can edit Primitives (raw hex) only. Semantic, Theme, Regulatory, Component stay locked to the design system team. Same permission matrix as v3.1.
Every phase produces a checkable artefact. No "looks good in Figma" — everything lands in a prototype or a CSS bundle that can be diffed.
| Phase | Check | Pass condition |
|---|---|---|
| Snabbare flatten | hex parity vs v3.1 export | byte-for-byte identical |
| Two-brand Semantic | CTA renders correctly on 4 combos (lyllo/snabbare × light/dark) |
matches visual regression baseline |
| Pipeline output | Style Dictionary → snabbare-dark.cssdiffed against hand-written theme.css |
zero semantic differences · hex-equivalent |
| Prototype integration | Global Search at localhost:5180<html data-brand="snabbare"> + .dark |
Snabbare green primary · correct dark chrome |
| Full system | 9 brands × 2 themes = 18 bundles | all pass visual regression |
Modes where designers bind. Primitives flat where devs inspect. Theme, Regulatory, Component preserved. One axis per collection. No magic.
Design System · ComeOn · v4 is a proposal, not shipped · based on Figma file AcbohbUnh3brJw52d3YlY6