← Back to blog

Theming and Brand Customisation at Scale with Design Tokens

How design tokens power white-label trading platforms across dozens of clients - token architecture, runtime switching, and maintaining consistency at scale.

Oliver Benns
Oliver Benns

Software engineer · Creator of Hedge UI


When a single trading platform serves dozens of clients, each with their own brand identity, theming stops being a CSS exercise and becomes an architecture problem. Hard-coded colours become unmaintainable. One-off overrides multiply. The design system drifts until no one is sure what the "correct" value for a border colour is.

Design tokens solve this by creating a single source of truth for every visual decision, and making those decisions swappable at runtime.

What design tokens are

A design token is a named value that represents a design decision. Instead of writing #3b82f6 in your component, you reference --color-primary. Instead of 16px, you reference --spacing-4. The token name describes the intent, and the value can change based on context (brand, theme, density).

For a trading platform, the token set typically covers:

interface ThemeTokens { // Brand colorPrimary: string; colorPrimaryForeground: string; // Surfaces colorBackground: string; colorCard: string; colorPopover: string; // Text colorForeground: string; colorMutedForeground: string; // Trading-specific colorBuy: string; colorSell: string; colorProfit: string; colorLoss: string; colorChartGrid: string; colorChartCrosshair: string; // Borders colorBorder: string; colorBorderActive: string; // Typography fontSans: string; fontMono: string; // Spacing and sizing radiusSm: string; radiusMd: string; radiusLg: string; }

Notice the trading-specific tokens. A generic design system has "success" and "danger" colours. A trading design system needs "buy", "sell", "profit", and "loss" because these are not the same concept - some clients want green for buy and red for sell, while others use the opposite convention.

Token architecture

Organise tokens in three layers:

1. Primitive tokens

Raw values with no semantic meaning:

{ "blue-500": "#3b82f6", "blue-600": "#2563eb", "green-500": "#22c55e", "red-500": "#ef4444", "gray-900": "#111827", "gray-800": "#1f2937" }

2. Semantic tokens

Assign meaning to primitive values:

{ "color-primary": "{blue-500}", "color-background": "{gray-900}", "color-buy": "{green-500}", "color-sell": "{red-500}" }

3. Component tokens

Map semantic tokens to specific component contexts:

{ "order-form-buy-button-bg": "{color-buy}", "order-form-sell-button-bg": "{color-sell}", "order-book-bid-text": "{color-buy}", "order-book-ask-text": "{color-sell}" }

Components reference component tokens. Clients override semantic tokens. Primitive tokens provide the palette. This three-layer structure means a client can change their brand colour by overriding a single semantic token, and it cascades through every component.

Runtime theme switching

For a white-label platform, themes need to be applied at runtime based on the tenant. CSS custom properties make this straightforward:

function applyTheme(tokens: ThemeTokens) { const root = document.documentElement; Object.entries(tokens).forEach(([key, value]) => { const cssVar = `--${camelToKebab(key)}`; root.style.setProperty(cssVar, value); }); } function camelToKebab(str: string): string { return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(); }

Call this on app initialisation after loading the tenant configuration. The entire UI updates instantly without re-rendering React components.

Tailwind CSS v4 integration

Tailwind CSS v4 uses CSS custom properties natively. Define your theme variables in your CSS and reference them with utility classes:

@theme { --color-primary: var(--color-primary); --color-buy: var(--color-buy); --color-sell: var(--color-sell); --color-profit: var(--color-profit); --color-loss: var(--color-loss); }

Components use standard Tailwind classes that automatically resolve to the active theme:

<button className="bg-buy text-white">Buy</button> <button className="bg-sell text-white">Sell</button> <span className="text-profit">+$1,234.56</span>

No conditional class names. No theme context. The CSS layer handles everything.

For a starter kit it is reasonable to collapse the three-layer model into two. There is no need for a primitive layer of your own (--blue-500, --green-500); Tailwind's built-in OKLCH palette is already the underlying colour source, and you can define semantic tokens at the :root and .dark level alone. So --bid resolves to var(--color-green-950) in dark mode and var(--color-green-100) in light mode, and component classes like bg-bid and text-ask-foreground pick up the right value automatically. For a 20-client white-label deployment we would add the primitive layer back in to give clients a palette to reach into; for a starter kit and the teams that fork it, the two-layer shape is enough and ships with less indirection.

Managing tokens across clients

With 20+ clients, you need a system for managing token overrides. Store client themes as JSON that extends a base theme:

const baseTheme: ThemeTokens = { colorPrimary: "#3b82f6", colorBuy: "#22c55e", colorSell: "#ef4444", // ... all defaults }; const clientThemes: Record<string, Partial<ThemeTokens>> = { "client-a": { colorPrimary: "#8b5cf6", // Everything else inherits from base }, "client-b": { colorPrimary: "#f59e0b", colorBuy: "#ef4444", // This client uses red for buy colorSell: "#22c55e", // And green for sell }, }; function getThemeForClient(clientId: string): ThemeTokens { return { ...baseTheme, ...clientThemes[clientId] }; }

Most clients only override 3-5 tokens. The base theme does the heavy lifting.

Contrast validation

When clients choose their own brand colours, contrast issues are inevitable. A bright yellow primary colour on a dark background might look great in a mockup but fail WCAG contrast requirements on button text.

Build automated contrast checking into your theme pipeline:

function validateThemeContrast(tokens: ThemeTokens): ContrastIssue[] { const issues: ContrastIssue[] = []; const pairs = [ ["colorPrimary", "colorPrimaryForeground", 4.5], ["colorBackground", "colorForeground", 4.5], ["colorCard", "colorMutedForeground", 3.0], ["colorBuy", "colorBackground", 3.0], ["colorSell", "colorBackground", 3.0], ] as const; for (const [bg, fg, minRatio] of pairs) { const ratio = getContrastRatio(tokens[bg], tokens[fg]); if (ratio < minRatio) { issues.push({ bg, fg, ratio, required: minRatio }); } } return issues; }

Run this check when a new client theme is created and flag issues before they reach production.

Dark mode and light mode

Most trading platforms are dark mode only. But some clients want to offer a light mode option. Design tokens make this a second set of overrides:

const darkTokens: ThemeTokens = { colorBackground: "#0a0a0a", colorCard: "#171717", colorForeground: "#fafafa", // ... }; const lightTokens: Partial<ThemeTokens> = { colorBackground: "#ffffff", colorCard: "#f5f5f5", colorForeground: "#171717", // ... };

The client theme overrides apply on top of whichever mode is active. This means you have three layers: mode (dark/light), base theme, and client overrides.

Typography tokens

Some clients have brand fonts that need to load before the UI renders, or the text will flash when the font arrives. Handle this with font loading detection:

async function loadClientFont(fontFamily: string, fontUrl: string): Promise<void> { const font = new FontFace(fontFamily, `url(${fontUrl})`); await font.load(); document.fonts.add(font); }

Call this before applying the theme, and only set the font token after the font has loaded. This prevents layout shifts from font swapping.

Testing themed components

Every component should be tested with at least the base theme and one client override. Storybook is useful here - create a theme decorator that lets you preview components with different token sets:

const themeDecorator = (Story: React.ComponentType, context: StoryContext) => { const theme = context.globals.theme ?? "default"; const tokens = getThemeForClient(theme); useEffect(() => { applyTheme(tokens); }, [tokens]); return <Story />; };

Combine this with visual regression testing to catch cases where a client's colour choice breaks a component's visual hierarchy.

The payoff

A well-structured token system turns client onboarding from a multi-day design and engineering task into a 30-minute configuration exercise. Define 5 colour overrides, upload a logo, and the entire platform looks like it was built for that client. At scale, that efficiency is the difference between a profitable white-label business and one that drowns in per-client customisation work.

Kickstart Your Trading Application

Hedge UI is a React starter kit with production-ready trading components, real-time data handling, and customisable layouts — so you can ship faster.

Get Hedge UI