01 / 19
20-Brand White Label · ComeOn · 2026-04-20

Design Token
Architecture v3

The standalone design-token architecture for ComeOn's 20-brand white-label platform. Five collections, brand × theme × jurisdiction axes, 4-point scale, nine-brand edge-case audit absorbed into the semantic and component layers.

5 collections 20 brand modes 2 themes 7 jurisdictions ~585 tokens marketing-editable

→ Brand Audit (evidence) → Component Audit → Token Reference v3

v3 is the current architecture. v2 is preserved in the Archive as the record of how we got here.

02 Why v3

v2 described the shape. v3 describes the edges.

v2 locked the 5-collection model, the 4-point scale, the four axes (brand × theme × jurisdiction × viewport). But the brand audit across 9 live brands surfaced recurring divergences that v2's semantic layer can't cleanly express today.

What v2 didn't cover

  • 3-way icon split (Big / Descriptive / Action) happens in 4 of 9 brands; implicit in v2, not named.
  • Menu row needs independent tokens for label-icon vs chevron — no component alias layer exists.
  • Pill has no pill.* semantic family; it borrows from Sub Menu/*.
  • CTA radius varies by context (form vs wallet vs landing) but contexts aren't named modes.
  • "View all" is a pattern, not a component; its label/icon split is invisible.
  • Landing has a third-accent Tag on 5 of 9 brands — getlucky blue, reviant purple, lyllo pink — with no guidance that this is deliberate.

What v3 adds

  • 3 new Semantic families (collection 3): pill.*, badge.*, banner.overlay/on-surface.
  • 1 new Semantic role: color.icon.nav-selected · 2 new radius roles: radius.modal, radius.badge.
  • 4 new Component alias packs (collection 5): menu-row.*, view-all.*, nav.selected-* split, cta.* with context variants.
  • Header formalised from Lobby Header/* into header.* component aliases.
  • CTA Radius is promoted to context variants (form / card / wallet / landing) resolved via Figma component variants, not a new collection axis.
  • Collection 5 (Component) grows from v2's <10 tokens to ~35 proper alias packs. Locked from marketing edits; Brand modes on collection 1 remain the only marketing edit surface.
  • A "README" frame inside Figma listing intentional splits, so nobody "fixes" them.
03 The 5-collection model

Same five collections as v2. Different contents.

Brand is a mode on Collection 1, not its own collection. v3 adds new roles inside Semantic (collection 3), and expands Component (collection 5) from v2's <10 last-resort tokens into proper alias packs.

1 · Primitives 20 brand modes
raw colour ramps · typography families · radius · layout · gradients · 4-pt base
2 · Theme 2 modes · light / dark
~25 neutral-flip tokens · surface · text · border · overlay
3 · Semantic +10 roles in v3 · global
text.* · surface.* · icon.* · pill.* · badge.* · radius.* · banner.* · iGaming families
4 · Regulatory 7 jurisdiction modes
default · SE · DK · MT · ON · NL · PL · reg/rg/* · reg/bonus-disclaimer/* · reg/age-gate/*
5 · Component expanded in v3 · global
menu-row.* · view-all.* · nav.selected-* · cta.* with variants · header.*

Brand isn't a separate collection — it's the 20-mode axis on Primitives. Theme lives as 2 modes on Collection 2. Jurisdiction as 7 modes on Collection 4. Semantic and Component are global (no modes) and compose everything downstream.

04 Implementation diagram

How a single token resolves.

One menu-row chevron. Brand getlucky, theme dark, jurisdiction NL. The resolution chain, five collections, final hex.

Component master
Menu Item
chevron · shape fill
5 · Component
menu-row
.action-icon
alias pack · global
3 · Semantic
color.icon
.action
role · global
1 · Primitives
color/brand/
primary-600
mode: getlucky
#ff993d
4 · Regulatory mode NL — no override on color.icon.actionfalls through ↓
2 · Theme mode dark — icon.action doesn't flip per theme — falls through ↓

Figma modes are per-frame

Each frame picks one mode per mode-carrying collection. Primitives · Theme · Regulatory each have a dropdown; Semantic · Component follow automatically.

Aliases across collections

Semantic points to a Primitive. Flip the brand mode → the Primitive returns a different hex → every Semantic + Component token downstream re-renders.

Regulatory narrows

Only ~15 tokens (rg · bonus disclaimer · age gate) have reg entries. Everything else falls through to Primitives as usual.

05 Where each mode lives

One mode axis per collection.

Fused into one collection, 20 × 2 × 7 = 280 modes per token. Split across collections, each axis multiplies only the tokens that actually vary along it.

1 · Primitives
2 · Theme
3 · Semantic
4 · Regulatory
Mode axis
Brand
20 modes
Theme
2 modes
Global
no modes
Jurisdiction
7 modes
Contents
ramps · radii · fonts · layout · gradients
surface · text · border · overlay (neutral flips)
text.* · surface.* · icon.* · pill.* · iGaming families
reg/rg/* · reg/bonus/* · reg/age-gate/*
Token count (approx)
~110 / brand
~25 (flip)
~345
~22
v3 additions
+10 roles
5 · Component global · no modes · expanded in v3
menu-row.* · view-all.* · nav.selected-* · header.* · cta.* (with 4 context variants) — was ~8 tokens in v2, ~35 in v3

Frame picks a mode per collection independently. Figma resolves cross-collection aliases at render time. No single token stores 280 values; each stores only the values for the axis it sits on.

06 Edge case → Collection

Every v3 edge case lives in a specific collection.

The answer to "where does this go?" for each of the 8 patterns from the Brand Audit.

Edge caseNew token(s)CollectionWhy there
Three-icon split (Big / Descriptive / Action)color.icon.* already exist; no change3 · SemanticSemantic roles, brand-moded via Primitives alias
Menu-row icon ≠ chevronmenu-row.icon, menu-row.action-icon5 · ComponentComponent alias pack — brand sees two split tokens in mode panel
Pill family missingpill.surface, pill.on-surface, pill.border, pill.*.selected3 · SemanticRole-level semantic tokens, aliased per-brand via Primitives
Badge formalisedbadge.on-surface, badge.border, radius.badge3 · SemanticExtends existing Main/Badge into a family
Modal radius independentradius.modal3 · SemanticOne role; brand mode sets the value via Primitives alias
CTA radius varies by contextcta.radius with 4 variants (form/card/wallet/landing)5 · ComponentFigma component variants, not a mode axis — each CTA variant binds to a different radius Primitive
Bottom-nav selected splitnav.selected-icon, nav.selected-text5 · ComponentComponent alias pack — reviant-A splits them; all other brands default equal
Header isolated from pageheader.* alias pack (9 tokens)5 · ComponentFormalises the existing Lobby Header/* primitives into a component-layer namespace
View-all label ≠ iconview-all.label, view-all.icon5 · ComponentComponent alias pack — evolves v2's interactive/viewall/*
Landing third-accent TagKeep existing Landing/Tag as is3 · SemanticAlready its own role in v2; v3 just documents it as intentional
Banner overlay / on-surfacebanner.overlay, banner.on-surface3 · SemanticCompletes the banner family so hero cards stop using inline fills
07 Edge case 01

Three icon types, independently settable.

"Big" (content illustrations), "Descriptive" (small icons labelling menu rows), "Action" (chevrons/arrows). 4 of 9 brands diverge them.

v3 semantic roles

// collection 2 · semantic
color.icon.content      → Main/Big-Icons
color.icon.descriptive  → Main/Icons
color.icon.action       → Main/Action-Icons
color.icon.nav-selected → Bottom Navbar/Selected-Icon (new)

Evidence in the wild

BrandContentDesc.Action
reviant (B)#5330ff#ff878f#ff4655
reviant (A)#2e3236#b18378#e71f00
getlucky#333333#ff993d#ff993d
lyllo#fd6a00#fd6a00#f02884
888.nl#ffffff#ffffff#f8f9fd

Rule

Never alias icon.action = color.primary at the semantic layer. Default them equal in brand mode, so any brand can un-merge without a schema change.

08 Edge case 02

Menu row: icon ≠ chevron (without editing the master).

Your first reported concern. In the deposit flow / profile, the leading icon and trailing chevron need to be tintable independently per brand. v3 achieves this through component aliases.

v3 component aliases (collection 3)

// collection 3 · component
menu-row.icon         = color.icon.descriptive
menu-row.action-icon  = color.icon.action
menu-row.label        = text.primary
menu-row.label-muted  = text.supportive

// collection 4 · brand mode: getlucky
color.icon.descriptive = #ff993d // orange
color.icon.action      = #ff993d // orange
// → merged, looks like one token, still two

// collection 4 · brand mode: reviant (A)
color.icon.descriptive = #b18378 // brown
color.icon.action      = #e71f00 // red
// → split on this one brand, zero master edits

Why this pattern works

  • The Menu Item master points at two different aliases, not at color.primary.
  • Marketing designers see two named tokens in the brand-mode panel — the split is discoverable.
  • If the two resolve to the same value, the UI looks merged; if they diverge, the UI splits. No code.
  • Locked collection 3 means a designer can't accidentally delete the alias and break the row.

Same pattern applies to

  • view-all.label / view-all.icon
  • nav.selected-icon / nav.selected-text
  • cta.label / cta.icon (when CTA has leading icon)
09 Edge case 03

Pills get their own semantic family.

Today: Pill fills reach for Sub Menu/* or Main/Badge. That's why pills look accidentally different across brands. v3 gives them a dedicated family and rewires the masters.

v3 additions

// collection 2 · semantic · new
pill.surface             // default state bg
pill.on-surface          // label + icon
pill.border              // outline-style brands
pill.surface.selected
pill.on-surface.selected
pill.surface.hover
radius.pill              → Extras/Pill Radius

// collection 3 · component · new
filter-pill.*   // alias pack for filter variant
nav-pill.*      // alias pack for navigation variant

Migration path (non-breaking)

  1. Add the 7 new semantic tokens, default them to alias the current borrowed values (pill.surfaceSub Menu/Default).
  2. Repoint Pill masters to the new tokens. Visually unchanged on day one.
  3. In each brand mode, override the pill tokens where identity diverges (mobilespin yellow-on-red, comeon mint-on-dark, reviant-B outline).
  4. Reclaim Sub Menu/* for sub-menus only. Drop the cross-usage.

Why outline-style needs pill.border

reviant (B) uses a stroked pill (ff4655 border) with a transparent interior — that's a legitimate design decision, but today it's achieved by inline override. A pill.border token lifts it into the system.

10 Edge case 04

CTA radius is already context-scoped. Name the contexts.

Brand lyllo's Button component resolves Extras/CTA Radius = 3, while its Wallet CTA and Landing CTA both resolve to 100. The token is already context-switchable; the contexts just aren't named.

v3 rename + mode-switch

// collection 2 · semantic
radius.cta.form     // input-adjacent, small radius
radius.cta.card     // inside a card, match card r
radius.cta.wallet   // wallet/checkout, fully rounded
radius.cta.landing  // marketing hero, fully rounded

// collection 4 · brand mode: lyllo
radius.cta.form    = 3
radius.cta.card    = 12   // match card
radius.cta.wallet  = 100
radius.cta.landing = 100

// collection 4 · brand mode: reviant (B)
radius.cta.* = 0  // all contexts sharp

All radius roles in v3

RoleTypicalNotes
radius.cta.form3renamed from Extras/CTA Radius
radius.cta.card12match card
radius.cta.wallet100pill-style CTA
radius.cta.landing100pill-style CTA
radius.card12unchanged
radius.input3unchanged
radius.pill100unchanged
radius.badge4new
radius.modal16new
radius.navigation0 or 12unchanged
11 Edge case 05

The header is its own identity.

snabbare lives the loudest version: blue header with green brand everywhere else. This already works today because Lobby Header/* tokens are isolated. v3's job is to enforce the boundary.

v3 rule

  • Nothing inside the Lobby Header component may bind to color.primary.
  • Everything inside the header binds to header.surface.* / header.on-surface.*.
  • The header's accent badge binds to header.accent.surface — separate from color.primary by design.
  • comeon is the only brand where header.surface = surface.page; v3 keeps them as distinct tokens with equal values.

v3 header aliases (collection 3)

// collection 3 · component
header.surface              → Lobby Header/Background
header.surface-1            → Lobby Header/Surface-1
header.surface-2            → Lobby Header/Surface-2
header.accent.surface       → Lobby Header/Surface-Primary
header.secondary.surface    → Lobby Header/Surface-Secondary
header.on-surface           → Lobby Header/Text
header.on-accent            → Lobby Header/On-Surface-Primary
header.active-selected      → Lobby Header/Active-Selected
header.ios-bar              → Lobby Header/IOS-Bar
12 Edge case 06

Landing is a sub-system with a third-accent Tag.

5 of 9 brands paint the Landing Tag with a colour that isn't Primary or Text-Primary — a third brand accent. Keep it as its own token, never alias it.

BrandPrimary CTASecondaryIcon CTA (tertiary)Tag
lyllo#f02884#ffffff#ffffff#f02884
snabbare#12ce67#ffffff#ffffff#ef7171 (3rd)
mobilebet#57b902#ffffff#1e2021#57b902
comeon#39f56a#3c423c#c0f9c8#c3ff00 (3rd)
mobilespin#d6112b#ffffff#ffffff#ffee00
getlucky#ff993d#ff993d#ffffff40#639bf4 (3rd)
reviant (A)#e71f00#cbaca5#e71f00#b18378 (3rd)
888.nl#fce403#212121#212121#fce403
reviant (B)#ff4655#ffffff#ff878f#5330ff (3rd)

v3 keeps the existing Landing/* family intact, no new tokens needed here, but documents these third-accent tags as intentional so marketing designers know to source them independently when theming.

13 Edge case 07

Bottom-nav split + the elevated brand.

Only reviant (A) splits selected-icon ≠ selected-text

Selected icon resolves #e71f00 (red), selected text resolves #2e3236 (dark). Every other brand keeps them equal. Keep the tokens split; default them equal in brand mode.

Only reviant (A) elevates + rounds the nav

Extras/Navigation Radius = 12, Navigation Shadow = #0000001a. Distinctive brand signature; must stay independent of card/pill radius.

v3 component aliases

// collection 3 · component
nav.background        → Bottom Navbar/Background
nav.default-icon      → Bottom Navbar/Default
nav.default-text      → Bottom Navbar/Default
nav.selected-icon     → Bottom Navbar/Selected-Icon
nav.selected-text     → Bottom Navbar/Selected-Text
nav.radius             → Extras/Navigation Radius
nav.shadow             → Extras/Navigation Shadow

// collection 4 · brand mode: reviant (A)
nav.selected-icon = #e71f00
nav.selected-text = #2e3236
nav.radius        = 12
nav.shadow        = #0000001a
14 Marketing-designer editability

One layer edits. Four layers protect.

Marketing designers edit brand-mode values on Collection 1 — and nothing else. Every other collection is locked.

Ownership matrix

CollectionModesEditorPermission
1 · Primitives20 brand modesMarketing + DSEdit brand-mode values
2 · Theme2 (light / dark)DS teamRead-only (rarely changes)
3 · SemanticglobalDS teamRead-only
4 · Regulatory7 jurisdictionsCompliance + DSEdit with review
5 · ComponentglobalDS teamRead-only

Marketing designers never touch the Semantic or Component layers — they touch color/brand/primary-600 under the brand mode, and every semantic/component token downstream re-renders.

Six rules for editability

  1. Edit brand-mode values only (the 20-mode axis on Collection 1). All other layers read-only.
  2. Role names, never colour names. text.primary not text.orange.
  3. Paired aliases for divergence. view-all.label + view-all.icon in Collection 5, default equal, split via brand-mode values.
  4. A pinned "brand editor" page in Figma with every component + brand-mode token side-panel.
  5. A README frame listing intentional splits (lyllo dual-palette, reviant-A nav split, etc.).
  6. Component constraints. Master fills bind to tokens; colour-picker disabled at the shape level.

"15-minute" re-theme test

Give a marketing designer an invented brand (name + 2 hex values), ask them to build a brand mode from scratch. Goal: working theme in under 15 minutes using only the brand-mode panel on Collection 1, no component edits, no semantic edits. If they hit a wall, that's the next semantic role v4 needs.

15 Migration from v2

Non-breaking. Four steps.

Step 1

Add tokens

In Collection 3 · Semantic, add the new roles (pill.*, badge.*, radius.modal, color.icon.nav-selected, banner.overlay). In Collection 5 · Component, add the alias packs (menu-row.*, view-all.*, nav.*, header.*, cta.*). Default each to alias today's value.

Step 2

Rewire masters

Repoint Pill, Menu Item, View-All, Bottom Navbar, Button, Banner masters from their current tokens to the new Collection 5 aliases. For CTA, also create 4 component variants (form / card / wallet / landing) each binding to its own radius.cta.* token. Verify pixel-parity on the 9 brand rows before merging.

Step 3

Brand-mode values

In Collection 1 · Primitives, under each of the 20 brand modes, populate the new ramp stops that the v3 semantic roles alias through. Overrides only where a deliberate split is required (reviant-A nav.selected-iconnav.selected-text). Leave the rest defaulted.

Step 4

Lock + document

Lock Collections 2, 3, 5 to read-only for non-DS editors. Keep Collection 1 editable by Marketing (brand-mode values only) and Collection 4 gated by Compliance. Add README frame with intentional-splits list. Publish branch; invite marketing team to validate.

Zero visual regression guarantee

Every new token defaults to an alias of today's value. Rendering is unchanged until a brand mode explicitly overrides. The switchover is additive, never subtractive.

16 Verification

How we know v3 is correct.

Per-brand regression checks

  • getlucky: menu icon pink ≠ chevron orange (or equal, documented)
  • reviant (A): nav selected-icon red ≠ text dark
  • reviant (B): cards + inputs + pills radius = 0; CTA landing = 0
  • mobilespin: chevrons black, not red
  • snabbare: header bg blue, brand CTAs green
  • 888.nl: icons white, CTAs yellow
  • lyllo: view-all label orange, chevron pink

System-level checks

  • Invent a dummy brand, fill only brand-mode values — every component re-skins without master edits.
  • Log in as "marketing designer" role, try to edit a primitive — must fail.
  • Try to edit a brand-mode token — must succeed.
  • Re-theme test: a marketing designer does a cold re-theme in < 15 min, zero docs.
  • No component in the library binds to Sub Menu/* outside the sub-menu scope (pill rewire complete).
  • Every new token has a README entry if its divergence is brand-specific and intentional.
17 v2 ↔ v3

v3 stands alone. v2 is the history.

The 5-collection model, the axes, the 4-point scale — all inherited from v2 and unchanged. v3 extends the contents of Collections 3 (Semantic) and 5 (Component). v2 lives in the Archive as the record of how we got here.

Carried over from v2, unchanged

  • The 5-collection model
  • The 4 axes (brand × theme × jurisdiction × viewport)
  • The 4-point scale linked to 1rem = 16px
  • SemVer, quarterly cadence
  • Jurisdiction modes (SE · DK · MT · ON · NL · PL + default)
  • The 20-brand scaling story
  • All iGaming semantic families (odds, betslip, casino, VIP, launcher, etc.)

→ Archive: Architecture v2 (for historical reference)

v3 adds

  • Collection 3 · Semantic: 10 new roles (pill.*, badge.*, radius.modal, radius.badge, color.icon.nav-selected, banner.overlay, banner.on-surface)
  • Collection 5 · Component: expanded from v2's <10 tokens to ~35 alias packs (menu-row.*, view-all.*, nav.*, header.*, cta.*)
  • CTA radius becomes a Figma component variant axis (form / card / wallet / landing) — not a mode axis
  • Marketing-designer permission matrix: edit brand-mode values on Collection 1 only
  • Intentional-splits README convention inside the Figma file
  • Nine-brand audit evidence feeding every decision

→ Open Token Reference v3 (full catalogue)

18 Open questions

Things worth deciding before rollout.

Do we expose Jurisdiction to marketing?

Today collection 5 is DS + compliance only. Some campaigns may need jurisdiction-specific overrides (e.g. NL requires bonus-cap disclosures inline). Decide: do they fork brand modes or jurisdiction modes?

Deprecate Sub Menu/* borrowing?

Once Pill is rewired, should we aggressively delete pill usage of Sub Menu/* or leave the old bindings for one release as a safety net?

"ViewAll" as a component or a style?

Make it a proper component (reusable, versionable) or leave it as a pattern with a named style pair? Trade-off: component = strict consistency; style = more flexibility for editorial placement.

How do we document intentional splits?

README frame inside Figma? Separate Confluence page? Inline description on each token? Propose: a README frame inside the Figma file is closest to the work; Confluence is a secondary source.

End of v3 · 2026-04-20

Ready for implementation.

9 brands audited. 10 new roles. 4 new alias packs. One non-breaking path to rollout. The next step is the Figma variables work.

Brand Audit Component Audit Reference v3 Architecture v2 Home

Design System · ComeOn · v3 builds on v2 · based on Figma file qnPOhVnKHuYo7t0g6PgrcS