A 5-collection, multi-axis Figma Variable system for brand, theme, jurisdiction, and iGaming-specific surfaces — built on a 4-point scale linked to 16px body.
→ Token Reference · all tokens in tables
Confidential · Design System Documentation · 2026-04-17
The v1 doc treated "brand" as the only dimension of variation. The real shape is four axes, and v2 splits them so the matrix stays tractable at scale.
18 modes × 410 tokens → ~7,380 values. Theme hidden in "neutral-50 or neutral-950" strings. No regulatory story. No scaling discipline. No density story.
Brand (20) × Theme (2) × Jurisdiction (7) + density embedded in layout primitives. 4-point scale linked to 1rem = 16px. ~550 tokens, ~2,305 distinct values.
A single surface/page token needs to resolve differently based on four independent questions:
Fused into one axis, you'd manage 20 × 2 × 7 = 280 modes on every token. Split across collections, each axis multiplies only the tokens that actually vary along it. Theme touches ~25 tokens, Regulatory ~15, Brand ~90. The math stays manageable.
| Axis / Scope | Decision |
|---|---|
| Brand count | 20 live brands v1's 18 was stale |
| Theme | separate axis every brand supports light + dark |
| Regulatory | 7 modes default · SE (Spelinspektionen) · DK (Spillemyndigheden) · MT (MGA) · ON (iGO/AGCO) · NL (Kansspelautoriteit) · PL (MF) |
| Scale base | 4px · 1rem = 16px all spacing, radius, type, layout are multiples of 4 · rem-linked |
| Density | brand-embedded in layout primitives + responsive. No user-toggle. |
| RTL | LTR only no logical-direction tokens |
| Tooling | Figma Enterprise + native Variables chained collections, no plugins |
| Aggregator chrome | full chrome tokens top bar, bottom bar, loading, error, live-casino controls |
| VIP / Loyalty | Primitives + Semantic tier colors brand-moded; badge semantics shared |
| Versioning | SemVer · quarterly minor deprecations one release ahead |
| Ownership | single DS lead bus-factor risk → mitigate with TOKENS.md |
Each collection owns one mode axis. Frames select modes independently per collection. Figma resolves aliases across collections at render time.
Each layer references only the layer directly below. Semantic → Theme or Primitives. Regulatory → Semantic. Component → Semantic. Never skip a layer. Never alias upward.
Each collection carries one mode axis. A frame picks a mode per collection independently — Figma resolves cross-collection aliases at render.
# Frame-level mode selection (Figma right panel): Primitives → Brand: ComeOn! Theme → Dark Regulatory → SE # Alias chain for one resolved value: surface/page → theme/surface/page (Theme · Dark) → neutral-950 (Primitives · ComeOn!) = #0B0A1A
Axes are orthogonal: 20 × 2 × 7 = 280 render states, but each collection only stores its own mode set.
20 brand modes. Raw values only — never used directly in components.
v1 had nav/sidebar/width: 240 or 280 as a literal string in Semantic. That's not a semantic decision — it's a raw brand measurement. Promoting it to a brand-moded primitive lets Figma track it properly and lets layout/* align with the 4-point scale.
Same value across all 20 modes. All dimensions are multiples of 4px, exported as rem. Kept in Primitives for simplicity.
size (xs..4xl) · weight · line-height · letter-spacing · transform
spacing/1..20 (4px..80px)
all multiples of 4
opacity/0 · 10 · 20 · 40 · 60 · 80 · 95
xs · sm · md · lg · xl · 2xl
xs (16) · sm (20) · md (24) · lg (32) · xl (40)
from Component → now a primitive (stacking is foundational)
duration · easing · shadow/xs..overlay
compound: duration + easing pairs
Spacing/opacity/etc. don't vary by brand. Keeping them in Primitives avoids a sixth collection.
v1 hardcoded inline: surface/overlay: neutral-800 at 96% opacity. With an opacity/* primitive, the Semantic alias becomes a clean reference — no more magic numbers leaking into the intent layer.
One base unit links spacing, radius, typography, icons, and layout. Every dimension in the system is either a multiple of 4 or a documented exception.
| spacing/1 | 4 | 0.25rem |
| spacing/2 | 8 | 0.5rem |
| spacing/3 | 12 | 0.75rem |
| spacing/4 | 16 | 1rem |
| spacing/5 | 20 | 1.25rem |
| spacing/6 | 24 | 1.5rem |
| spacing/8 | 32 | 2rem |
| spacing/12 | 48 | 3rem |
| spacing/16 | 64 | 4rem |
| spacing/20 | 80 | 5rem |
| radius/none | 0 | 0 |
| radius/sm | 4 | 0.25rem |
| radius/md | 8 | 0.5rem |
| radius/lg | 12 | 0.75rem |
| radius/xl | 16 | 1rem |
| radius/2xl | 24 | 1.5rem |
| radius/full | 9999 | — |
v1's radius/xs: 2px is dropped — sub-4 breaks the scale. Min curve is now 4px.
| size/xs | 12 | 0.75rem |
| size/sm | 14 | 0.875rem* |
| size/md | 16 | 1rem |
| size/lg | 20 | 1.25rem |
| size/xl | 24 | 1.5rem |
| size/2xl | 32 | 2rem |
| size/3xl | 40 | 2.5rem |
| size/4xl | 48 | 3rem |
* size/sm: 14px is a sanctioned half-step — 12 and 16 don't cover body-secondary well. All others strict 4-point.
A designer should be able to hit the right value without a reference card: start from 16, go up or down in multiples of 4. If a value isn't on the scale, it doesn't get shipped — or it becomes a sanctioned exception documented in the variable description.
Figma Variables support Number, Color, String, Boolean — no CSS units. The solution isn't to fight it; it's to accept that Figma stores px and the export pipeline translates to rem.
spacing/4 = 16
Number (raw px)
rem = px / 16
Script / CSS calc / plugin
--spacing-4: 1rem;
What components consume
~20-line Node script hits the Figma Variables REST API, divides dimension values by 16, writes a CSS/JSON token file. Run manually on release or in CI. Figma stays clean (only numbers). CSS is semantic (only rem). Script is ~1 hour of work.
calc() wrapperExport raw JSON. Your CSS template uses calc(16 / 16 * 1rem) — the browser does the math at parse time. Zero pipeline logic, slightly redundant runtime. Good if you don't want any build step.
Every dimension variable gets a String sibling: spacing/4 = 16 + spacing/4/css = "1rem". Lives entirely in Figma — no pipeline. Drift-prone: two sources of truth per token. Only use if you cannot ship a script.
xs, sm, md, lg) or positional (1, 2, 3)spacing/16px or size/rem-11.5), not px or remem at consumption time, not remradius/full: ship as 9999px, not rem (pill shape must be absolute)Only the tokens that flip direction based on light/dark. References brand-moded Primitives for actual neutral values. Everything v1 wrote as "neutral-X or neutral-Y" lives here.
theme/surface/page light → neutral-50 dark → neutral-950 theme/surface/raised light → neutral-100 dark → neutral-900 theme/surface/elevated · sunken · inverse · skeleton · skeleton-shimmer · tooltip · code
theme/text/primary light → neutral-900 dark → white theme/text/secondary · muted · disabled · inverse · caption · heading-color theme/border/default · subtle · strong · inverse · divide theme/overlay/backdrop-opacity (if varies by theme)
Global (no modes). References Theme and Primitives. This is where components pull 99% of what they need. v1 sections 02–17 mostly land here.
surface · text · border · divide
primary · secondary · tertiary · destructive · viewall
success · warning · error · info · toast
header · sidebar · tab · bottom · breadcrumb
input · select · checkbox · radio · toggle · slider
modal · drawer · tooltip · popover · sheet
odds · live · betslip · result
casino · promo · rg · payment
see next slide
brand/primary | secondary | tertiary is fragile when brands disagree on what "tertiary" means. Consider role aliases in Semantic: brand/action, brand/accent, brand/support → pointing to the primary/secondary/tertiary primitives. Intent-named consumption, ramp-named definition.
Every group here was either entirely absent in v1 or under-specified. Most are iGaming-critical surfaces players spend real time on.
vip/tier/{bronze, silver, gold, platinum, diamond}/{bg, text, border, icon}
launcher/{top-bar, bottom-bar, close-icon, balance-text, deposit-shortcut, fullscreen-toggle, favorite-icon, session-timer, loading-bg, error-state, live-casino/mute-icon, live-casino/chat-icon}
sportsbook/bet-builder/{leg-default, leg-selected, correlation-warning}
sportsbook/accumulator/{row, boosted-bg, boosted-text, multiplier-badge}
casino/live-dealer/{studio-chrome, seat-count, min-max-chips, dealer-name, countdown}
casino/jackpot/{mini, minor, major, grand}/{bg, text, ticker}
notification · chat · geo-block · receipt · tournament
gaming/result/cashout-full + cashout-partial · feedback/toast/info-bg · warning-bg
An iGaming product served to a jurisdiction it's not licensed in is a legal incident. The geo-block page must be immediately recognizable and per-brand compliant. Tokenize it.
7 modes. Narrow scope: only tokens regulators actually constrain. Sits above Semantic in the alias chain so regulator requirements win over brand preference.
| Mode | Regulator | Key constraints |
|---|---|---|
| default | fallback | Standard brand-driven values; used when no jurisdiction override applies |
| SE | Spelinspektionen | Spelpaus self-exclusion prominence · deposit-limit bar · strict bonus-language rules · 18+ |
| DK | Spillemyndigheden | ROFUS self-exclusion · Spillemyndigheden seal · mandatory RG info · 18+ |
| MT | MGA | MGA seal placement · RG logo visibility · responsible-play reminder |
| ON | iGO / AGCO | ConnexOntario resource · strict ad restrictions · no celebrity endorsements · 19+ |
| NL | Kansspelautoriteit (KSA) | Cruks self-exclusion · deposit-limit mandatory · 24+ for ads · KOA compliance |
| PL | Ministerstwo Finansów | State-regulated scope · Gambling Act warnings · 18+ · restricted game types |
reg/rg/warning/{bg, text, border, icon, min-size-hint}
reg/rg/self-excl/{bg, text, cta-bg, program-name} → Spelpaus | ROFUS | Cruks | ConnexOntario | ...
reg/rg/limit/{bar-track, bar-fill, bar-fill-near, bar-fill-at}
reg/bonus-disclaimer/{text, min-contrast-hint, min-size-hint}
reg/logo/{color, hover, min-size-hint} → GamCare · Spelpaus · Stodlinjen · Cruks · ...
reg/age-gate/{min-age, text} → 18+ / 19+ (ON) / 24+ (NL ads)
Every regulator-gated token in Semantic must alias to its reg/* counterpart, not directly to Primitives. This ensures a jurisdiction switch actually takes effect. Engineers should never see status/warning-600 inside gaming/rg/warning/* — they should see reg/rg/warning/bg.
Under 10 tokens. Global. Only things that genuinely cannot live in Semantic or Primitives.
layout/* primitiveslayout/* in Primitivesradius/* (brand-moded)If this collection grows beyond 10 tokens, the Semantic layer is under-specified. Audit before adding. Every addition here is a new exception for engineers to remember; exceptions don't scale across 20 brands.
| Collection | Tokens | Modes | Values in play |
|---|---|---|---|
| 1 · Primitives | ~160 | 20 brand (of which ~90 brand-moded) | ~1,800 + globals |
| 2 · Theme | ~25 | 2 light/dark | ~50 |
| 3 · Semantic | ~340 | 1 global | ~340 |
| 4 · Regulatory | ~15 | 7 jurisdictions | ~105 |
| 5 · Component | ~8 | 1 global | ~8 |
| Total v2 | ~550 | — | ~2,305 |
Only brand-moded tokens multiply by 20. Theme multiplies by 2. Regulatory by 7. Semantic not at all.
Aliases flow downward only. Regulatory → Semantic → Theme/Primitives. Never upward, never sideways.
Components reference Semantic. Semantic references Theme or Primitives. Components may never reference Primitives directly — that's the whole reason Semantic exists.
For regulator-gated Semantic tokens, the alias must pass through Regulatory. gaming/rg/warning/bg → reg/rg/warning/bg, not directly to status/warning-100.
No hex codes, no opacity percentages, no pixel values in the Semantic layer. Everything resolves to a Primitive or Theme alias. If you can't express it that way, the Primitive is missing.
Figma doesn't enforce these rules for you. A designer can create a Semantic token that points directly to #FDCB6E. Policy + review catches that. A short TOKENS.md in the design repo, plus a quarterly audit, is cheap insurance against drift.
Token hygiene on the code side mirrors the alias chain. Components consume Semantic. Only platform code touches Primitives directly.
| Consumer | May read | May NOT read |
|---|---|---|
| UI components (Button, Card, Odds, etc.) |
semantic/* | primitive/* · theme/* · reg/* |
| Feature flows (Betslip, Deposit, KYC) |
semantic/* | primitive/* · theme/* · reg/* |
| Platform / layout shell (routing, chrome, error pages) |
semantic/* · component/* · primitive/layout/* · primitive/z-index/* · primitive/breakpoint/* | primitive/color/* |
| Design System internals | everything | — |
A simple ESLint/Stylelint rule can block primitive/color/* imports outside the DS package. The contract becomes self-enforcing without code review burden.
| MAJOR | Rename or remove a token. Change alias chain shape. Add a new mode axis. |
| MINOR | Add new tokens. Add values to existing modes. Add a new brand mode. |
| PATCH | Correct a value. Fix a typo. Update a description. |
# Example changelog entry ## 2.3.0 — 2026-07-15 (minor) ### Added - casino/jackpot/tier/{mini, minor, major, grand} — splits the singlejackpot-goldprimitive into four tiers per brand demand. - sportsbook/bet-builder/correlation-warning ### Deprecated (removal in 3.0) - casino/jackpot/text → usecasino/jackpot/tier/major/text### Fixed - NL mode: reg/age-gate/min-age corrected from 18 → 24 for ad surfaces
Update v1 PDF → v2: brand count 18 → 20, redraw architecture with 5 collections, document the 4-point scale. This file is your source.
Use one of the 2 transparent-header outliers. It will stress-test the system on day one, not day ninety.
All 5 collections. Both themes. SE jurisdiction (ComeOn heartland). Don't scale to 20 until this one is clean.
~20-line Node script reading the Figma Variables REST API, outputting CSS tokens with rem values. Validate output matches designer expectations.
opacity · breakpoint · icon/size · layout · gradient · transition · z-index · VIP tier colors. All on the 4-point scale.
VIP · launcher · bet-builder · live-dealer · jackpot tiers · tournament · notification · chat · geo-block · receipt · cashout split · toast info/warning.
Write the rules from slide 18 as a TOKENS.md. Share with engineering before any Figma Variables are declared "final."
SE first (core market, strictest day-to-day). Then NL (tight compliance). Then DK · MT · ON · PL. Each uses the same Regulatory pattern — only values differ.
Once the prototype + transform + contract are all clean: the remaining 19 brands are repetition, not design.
| Risk | Mitigation |
|---|---|
| Bus factor 1 on single DS lead | Write TOKENS.md now. Record a Loom walkthrough of the alias chain. Identify a backup. |
| Figma mode drift — a brand's Primitives diverge from the ramp spec | Quarterly audit script: export all Variables as JSON, diff brand ramps against spec, flag stops outside tolerance. |
| Scale discipline slips — a designer ships a 14px gap instead of spacing/4 | Transform script rejects non-multiple-of-4 values in dimension variables. Build-time error, not a lint warning. |
| px / rem transform breaks — root font-size overridden somewhere | Lock html { font-size: 100%; } in reset CSS. Document as non-negotiable. Test accessibility zoom at 200%. |
| Regulator changes mid-release — new KSA rule in Q3 | PATCH releases ship any time. Regulatory is a narrow collection; a 3-token patch is fast to land. |
| Engineering bypasses Semantic — imports a Primitive directly | Linting rule (slide 18). Code review. Make the contract a CI check, not a convention. |
| Figma native multi-mode UX — designers forget to set all three mode selectors | Document the three-selector checklist on every frame template. Provide starter file per brand with modes pre-configured. |
Don't try to build for 20 brands on day one. Build it right for one, on a 4-point scale, with a clean px-to-rem bridge. The architecture will carry the rest.
Complete token tables · every variable to create in Figma
Plan spec · ~/.claude/plans/users-zlatko-lazarov-desktop-tokens-des-peppy-parnas.md