
Building a Color System for Multi-Brand Products: A Practical Guide to Token-Based Palettes That Scale
by Julio Song
Open your design system's color tokens file. Now picture swapping every instance of your brand blue for a client's neon orange, and having everything still look polished, accessible, and intentional. If that thought makes you sweat, you're not alone.
Designers building white-label SaaS platforms, multi-tenant products, or enterprise tools that serve dozens (or hundreds) of brands run into a problem most color system guides ignore. Your color system can't be precious about any single color. It has to be a machine that takes an arbitrary brand primary and outputs a full, harmonious, accessible palette on the fly.
This guide shows how to build that machine: semantic token layers, automated palette generation, WCAG stress-testing, and the edge cases (yes, someone will hand you a yellow primary and expect it to work as a button color). At the end, four very different brand colors go through the system to show it works.
Why single-brand color guides fail multi-brand products
Most design system color guidance, whether it's Material Design, Apple's HIG, or popular Figma community resources, assumes you control the brand color. You pick it, tune it, and own it.
In multi-brand products, the brand color is an input, not a decision.
The tension is that you need visual consistency (your UI patterns must be recognizable and usable) while allowing radical chromatic flexibility. A healthcare client's teal and a sports brand's red must both feel native inside the same product shell.
When teams don't plan for this, the failure modes are predictable:
- Hard-coded color values scattered across components, so every new brand means a painful find-and-replace.
- Accessibility violations the moment a new brand color is dropped in, because contrast ratios were only verified for the original palette.
- Palettes that look "off" because secondary and neutral colors were hand-picked to complement only the original brand, not the new one.
The solution is a layered, token-based architecture where the brand color is a single input variable and everything downstream is derived algorithmically or through constrained mappings.
Architecting the token layers: from primitive to semantic
The foundation of a multi-brand color system is a three-layer token model.
Primitive tokens are raw color values with no opinion about usage, like blue-500: #3B82F6 or neutral-100: #F5F5F5. They're the generated output of your palette algorithm.
Alias (semantic) tokens are purpose-driven names. They describe what a color does, not what it looks like: color-action-primary, color-surface-elevated, color-text-on-primary.
Component tokens are scoped to specific UI elements: button-primary-bg, input-border-focus, badge-success-text.
The semantic layer holds it all together. Components never reference primitives directly. They reference semantic tokens, so swapping a brand means remapping the semantic layer to a new set of primitives while the components stay the same. Not one line of CSS in your button file needs to know that the brand switched from blue to orange.
A concrete naming convention:
color.action.primary: primary interactive color (buttons, links)color.action.primary.hover: hover state for primary actionscolor.feedback.success: success states, confirmationscolor.surface.default: standard page/card backgroundcolor.surface.subtle: slightly tinted background for sectionscolor.text.default: primary body textcolor.text.on-action: text that sits on top of action-colored backgrounds
Every name is brand-agnostic. No color.blue or color.coral anywhere.
The brand slot concept
Define a minimal set of brand inputs: typically a primary color, an optional secondary color, and an optional neutral preference (warm, cool, or auto). Derive everything else. The fewer inputs you require, the more the system scales. I call this the "brand input contract," and it's the API surface of your theming engine.
In practice, this maps cleanly to Figma variables (with modes for each brand), Style Dictionary or Tokens Studio for code-side token management, and CSS custom properties at runtime for dynamic theming.
Generating a full palette from a single brand primary
This is the algorithm. Take one hex value and produce a 10-step lightness scale (50 through 950) by manipulating lightness in OKLCH or LCH color space.
Why not HSL? Because HSL lies about perceptual uniformity. A "50% lightness" in HSL for yellow produces a bright, vivid color. The same 50% lightness for blue produces something that looks dramatically darker. OKLCH fixes this by modeling lightness the way human eyes actually perceive it.

Deriving secondary and accent colors
Use hue shifting in OKLCH: ±30° from the primary for analogous harmony, or ±150° for split-complementary schemes. Then generate their own 10-step scales. Because the relationship is geometric rather than hand-picked, harmony is baked in regardless of the starting hue.
Tinted neutrals
This detail is what separates a "theme" from a system. Instead of pairing your brand primary with generic gray, desaturate the brand primary to a very low chroma (OKLCH C value of 0.01 to 0.03). This produces "tinted neutrals" that subtly echo the brand without overwhelming the interface.
A coral brand gets warm, slightly rosy grays. A navy brand gets cool, steely grays. The effect is subtle, but it's the difference between a palette that feels cohesive and one that feels like a brand color duct-taped onto a generic UI.
Mapping scales to semantic tokens
Once you have the generated scales, the mapping is simple:
color-action-primary→ brand primary at step 500color-action-primary-hover→ step 600color-surface-subtle→ brand neutral at step 50color-surface-default→ brand neutral at step 0 (white) or step 950 (near-black for dark mode)color-text-default→ brand neutral at step 900color-text-on-action→ white or brand neutral 950, chosen dynamically based on the primary's luminance
Here's what that looks like with two very different brand primaries.
The structure is identical and the character is completely different, which is the goal.
Stress-testing for accessibility: automated contrast checks at scale
Every brand theme must meet WCAG 2.1 AA: 4.5:1 for normal text, 3:1 for large text and UI components. When you don't control the brand input, you can't hand-check. You need automated guardrails.
The contrast matrix
For every semantic pairing (text on surface, text on action, icon on surface-subtle, and so on), programmatically verify the contrast ratio. If a pairing fails, the system must either adjust the token mapping (use step 700 instead of 500 for the action color) or flag it for manual review.
The yellow problem
Extremely light or highly saturated primaries, like yellows, lime greens, and light pinks, often fail contrast as button background colors. The fix is to never use the raw primary for text. Always use it as a background with enforced dark text. color-text-on-primary is dynamically set to the darkest neutral or white based on the primary's luminance.
The dark primary problem
Very dark brand colors (near-black navys, deep maroons) collapse the lightness scale. Your 900 step becomes almost indistinguishable from your 700. Clamp minimum lightness differences between steps. Enforce at least a 5% OKLCH L difference between adjacent steps, redistributing the scale if necessary.
Tooling
The color.js library by Lea Verou handles OKLCH manipulation and contrast computation well. For Figma-based workflows, the Stark plugin can batch-check contrast across theme modes.
Handling edge cases: when brand colors break the system
However solid your algorithm is, real client colors will find the cracks. Plan for these before they arrive as panicked Slack messages from your client success team.
Clashing primary and secondary. Some clients provide a primary and secondary that are perceptually jarring together, like red and green at similar saturation. Override the client secondary with a derived secondary (hue-shifted from primary) and offer the client's original color as an "accent" with limited surface area.
Pure black or white as a "brand color." If primary lightness is above 90% or below 10% in OKLCH, inject a minimum chroma bump and lightness shift to ensure a usable scale can be generated.
Brand guidelines that mandate inaccessible pairings. Document a clear escalation path. Show the client the contrast failure data, offer the nearest accessible alternative (often just a minor lightness adjustment), and get written sign-off. Build this workflow into your theming tool's output as an automated report.
Brands with extensive existing palettes (10+ colors). You can't support arbitrary complexity at scale. Define your brand input contract, primary, optional secondary, optional neutral base, and map their extended palette into your semantic slots. Discard what doesn't fit. Fewer inputs are easier to maintain.
The system in action: four brands, one product
Take one product UI: a dashboard with cards, a data table, a primary action button, a navigation sidebar, and a status badge system. Run four brand primaries through the system.

Brand A: children's education platform, Sunny Yellow (#F5B731). The system shifts the primary from step 500 to step 600 for action elements to meet 4.5:1 contrast with white text. Dark text appears on all yellow surfaces. Warm, honey-tinted neutrals make the entire UI feel playful without becoming garish.
Brand B: fintech product, Deep Emerald (#1A7F5A). The desaturated green neutrals convey professionalism. A split-complementary accent in a muted rose provides just enough contrast for secondary actions and data visualization highlights.
Brand C: fashion/lifestyle brand, Hot Magenta (#D6246E). The system constrains this loud color to action tokens only. Tinted neutrals in a soft, warm pink-gray carry the brand warmth across large surfaces without visual fatigue.
Brand D: government/legal platform, Near-Black Navy (#0F1B2D). Scale clamping kicks in here. The system redistributes lightness steps to ensure enough differentiation across the dark end. The result is a restrained, authoritative palette with clear hierarchy.
For each brand, the product is unmistakably the same product. The layout, components, and interaction patterns are identical, but each version feels distinct and right for its brand.
Building and maintaining the system
Start with a theme config
Define the brand input contract as a simple JSON or YAML structure:
{
"brandPrimary": "#1A7F5A",
"brandSecondary": null,
"mode": "light",
"neutralWarmth": "auto"
}
Non-designers can fill this in, which matters.
Automate the pipeline
Write a generation script (Node.js with color.js or chroma.js, Python with coloraide) that takes the config, generates all primitive tokens, runs the contrast matrix, produces a pass/fail report, and outputs token files for Figma (via Tokens Studio sync), CSS custom properties, and platform-native formats for Swift and Kotlin.
Preview before you ship
Build a simple theme preview page, whether in Storybook or as a standalone HTML page, that renders all key components with the generated theme. Make it part of your client onboarding flow. "Upload your brand color, see your product in 30 seconds." Clients trust that kind of speed.

Governance checklist
Before any theme goes live, verify:
- Contrast matrix passes for all semantic pairings
- Color blindness simulation check (protanopia, deuteranopia, tritanopia) confirms that status colors like red and green for error and success are distinguishable
- Visual regression snapshot compared against the previous algorithm version to catch unexpected changes
Build the engine, not the palette
The goal of a multi-brand color system is a palette engine that makes any brand input look intentional. There's no perfect palette to find.
Structure matters more than any specific color. When your semantic tokens, lightness scales, and contrast guardrails are solid, even difficult brand colors come out usable and accessible.
Start with the three-layer token architecture. Generate your scales instead of hand-picking them, automate the accessibility checks, and plan for edge cases before they show up.
The four-brand run shows it working: one product, four very different brand identities, and no manual color tweaking. Once the machine works, you can trust it and spend your attention on the problems that actually need a human eye.
Written by
Designer and developer
Julio Song is a professional web designer and developer who builds and maintains ColorSift, and writes most of what is published here.
Related articles
How Figma's Variable Color Tokens Are Quietly Rewriting the Rules of Design Handoff
How Figma's variable color tokens are replacing static color styles and changing the way design teams hand color off to developers.
Borrowed Light: How to Build Interior-Inspired Color Systems for Residential App UIs in 2026
How to turn linen, terracotta and sage into accessible, token-based color systems for residential app UIs in 2026.
Colorblind by Default: How to Build a 5-Color UI Palette That Works for Everyone Without Looking Like a Safety Manual
A repeatable way to build a five-color UI palette that works for colorblind users and still looks like something a design team would ship.