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.
→ 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.
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.
pill.* semantic family; it borrows from Sub Menu/*.pill.*, badge.*, banner.overlay/on-surface.color.icon.nav-selected · 2 new radius roles: radius.modal, radius.badge.menu-row.*, view-all.*, nav.selected-* split, cta.* with context variants.Lobby Header/* into header.* component aliases.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.
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.
One menu-row chevron. Brand getlucky, theme dark, jurisdiction NL. The resolution chain, five collections, final hex.
color.icon.action — falls through ↓
Each frame picks one mode per mode-carrying collection. Primitives · Theme · Regulatory each have a dropdown; Semantic · Component follow automatically.
Semantic points to a Primitive. Flip the brand mode → the Primitive returns a different hex → every Semantic + Component token downstream re-renders.
Only ~15 tokens (rg · bonus disclaimer · age gate) have reg entries. Everything else falls through to Primitives as usual.
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.
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.
The answer to "where does this go?" for each of the 8 patterns from the Brand Audit.
| Edge case | New token(s) | Collection | Why there |
|---|---|---|---|
| Three-icon split (Big / Descriptive / Action) | color.icon.* already exist; no change | 3 · Semantic | Semantic roles, brand-moded via Primitives alias |
| Menu-row icon ≠ chevron | menu-row.icon, menu-row.action-icon | 5 · Component | Component alias pack — brand sees two split tokens in mode panel |
| Pill family missing | pill.surface, pill.on-surface, pill.border, pill.*.selected | 3 · Semantic | Role-level semantic tokens, aliased per-brand via Primitives |
| Badge formalised | badge.on-surface, badge.border, radius.badge | 3 · Semantic | Extends existing Main/Badge into a family |
| Modal radius independent | radius.modal | 3 · Semantic | One role; brand mode sets the value via Primitives alias |
| CTA radius varies by context | cta.radius with 4 variants (form/card/wallet/landing) | 5 · Component | Figma component variants, not a mode axis — each CTA variant binds to a different radius Primitive |
| Bottom-nav selected split | nav.selected-icon, nav.selected-text | 5 · Component | Component alias pack — reviant-A splits them; all other brands default equal |
| Header isolated from page | header.* alias pack (9 tokens) | 5 · Component | Formalises the existing Lobby Header/* primitives into a component-layer namespace |
| View-all label ≠ icon | view-all.label, view-all.icon | 5 · Component | Component alias pack — evolves v2's interactive/viewall/* |
| Landing third-accent Tag | Keep existing Landing/Tag as is | 3 · Semantic | Already its own role in v2; v3 just documents it as intentional |
| Banner overlay / on-surface | banner.overlay, banner.on-surface | 3 · Semantic | Completes the banner family so hero cards stop using inline fills |
"Big" (content illustrations), "Descriptive" (small icons labelling menu rows), "Action" (chevrons/arrows). 4 of 9 brands diverge them.
// 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)
| Brand | Content | Desc. | Action |
|---|---|---|---|
| reviant (B) | #5330ff | #ff878f | #ff4655 |
| reviant (A) | #2e3236 | #b18378 | #e71f00 |
| getlucky | #333333 | #ff993d | #ff993d |
| lyllo | #fd6a00 | #fd6a00 | #f02884 |
| 888.nl | #ffffff | #ffffff | #f8f9fd |
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.
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.
// 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
color.primary.view-all.label / view-all.iconnav.selected-icon / nav.selected-textcta.label / cta.icon (when CTA has leading icon)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.
// 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
pill.surface → Sub Menu/Default).Sub Menu/* for sub-menus only. Drop the cross-usage.pill.borderreviant (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.
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.
// 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
| Role | Typical | Notes |
|---|---|---|
| radius.cta.form | 3 | renamed from Extras/CTA Radius |
| radius.cta.card | 12 | match card |
| radius.cta.wallet | 100 | pill-style CTA |
| radius.cta.landing | 100 | pill-style CTA |
| radius.card | 12 | unchanged |
| radius.input | 3 | unchanged |
| radius.pill | 100 | unchanged |
| radius.badge | 4 | new |
| radius.modal | 16 | new |
| radius.navigation | 0 or 12 | unchanged |
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.
color.primary.header.surface.* / header.on-surface.*.header.accent.surface — separate from color.primary by design.header.surface = surface.page; v3 keeps them as distinct tokens with equal values.// 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
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.
| Brand | Primary CTA | Secondary | Icon 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.
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.
Extras/Navigation Radius = 12, Navigation Shadow = #0000001a. Distinctive brand signature; must stay independent of card/pill radius.
// 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
Marketing designers edit brand-mode values on Collection 1 — and nothing else. Every other collection is locked.
| Collection | Modes | Editor | Permission |
|---|---|---|---|
| 1 · Primitives | 20 brand modes | Marketing + DS | Edit brand-mode values |
| 2 · Theme | 2 (light / dark) | DS team | Read-only (rarely changes) |
| 3 · Semantic | global | DS team | Read-only |
| 4 · Regulatory | 7 jurisdictions | Compliance + DS | Edit with review |
| 5 · Component | global | DS team | Read-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.
text.primary not text.orange.view-all.label + view-all.icon in Collection 5, default equal, split via brand-mode values.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.
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.
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.
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-icon ≠ nav.selected-text). Leave the rest defaulted.
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.
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.
Sub Menu/* outside the sub-menu scope (pill rewire complete).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.
pill.*, badge.*, radius.modal, radius.badge, color.icon.nav-selected, banner.overlay, banner.on-surface)menu-row.*, view-all.*, nav.*, header.*, cta.*)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?
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?
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.
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.
9 brands audited. 10 new roles. 4 new alias packs. One non-breaking path to rollout. The next step is the Figma variables work.
Design System · ComeOn · v3 builds on v2 · based on Figma file qnPOhVnKHuYo7t0g6PgrcS