Back to docs
11Week 2

Multi-Brand Design System

One codebase, multiple identities — the token architecture that scales

What Are Design Tokens?

Design tokens are the smallest, most atomic pieces of your design system. They are named values that store visual design decisions — colors, spacing, typography, shadows, border radii — so that your entire UI can be updated from a single source of truth.

Instead of hardcoding #1a1a2e in 47 places, you store it once as a token and reference it everywhere. When the brand changes, you update the token — not 47 files.

Tokens bridge the gap between design and code. Designers define them in Figma, developers consume them in CSS or Tailwind, and both speak the same language.

The Token Hierarchy

A well-structured token system has three layers. Each layer serves a different purpose and builds on the one below it.

Layer 1 — Primitive Tokens (Global Palette)

These are raw values with no semantic meaning. They describe what the value is, not where it should be used.

/* Layer 1: Primitive Tokens */
--blue-500: #3b82f6;
--blue-600: #2563eb;
--blue-700: #1d4ed8;
--gray-50: #f9fafb;
--gray-100: #f3f4f6;
--gray-900: #111827;
--white: #ffffff;
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 16px;
--space-1: 4px;
--space-2: 8px;
--space-4: 16px;
--space-8: 32px;

Layer 2 — Semantic Tokens (Purpose-Based)

These tokens describe what the value is for, not what the value is. They reference Layer 1 tokens and give them meaning.

/* Layer 2: Semantic Tokens */
--color-bg-primary: var(--white);
--color-bg-secondary: var(--gray-50);
--color-bg-inverse: var(--gray-900);
--color-text-primary: var(--gray-900);
--color-text-secondary: var(--gray-500);
--color-text-inverse: var(--white);
--color-border-default: var(--gray-200);
--color-action-primary: var(--blue-600);
--color-action-primary-hover: var(--blue-700);
--radius-button: var(--radius-md);
--radius-card: var(--radius-lg);

Layer 3 — Component Tokens (Scoped)

These tokens are scoped to specific components. They reference Layer 2 tokens and provide component-level overrides.

/* Layer 3: Component Tokens */
--button-bg: var(--color-action-primary);
--button-bg-hover: var(--color-action-primary-hover);
--button-text: var(--color-text-inverse);
--button-radius: var(--radius-button);
--card-bg: var(--color-bg-primary);
--card-border: var(--color-border-default);
--card-radius: var(--radius-card);
--card-padding: var(--space-4);

Tip: Components should only use Layer 2 (semantic) tokens. If a component references --blue-600 directly, it cannot adapt when the brand changes. Always go through the semantic layer.

Setting Up Multi-Brand Tokens

The power of this architecture becomes clear when you need to support multiple brands from the same codebase. Each brand defines its own set of Layer 1 primitives, but they all map to the same Layer 2 semantic tokens.

globals.css — Brand Definitions

/* ============================
   Brand A — "Midnight"
   ============================ */
[data-brand="midnight"] {
  /* Primitives */
  --brand-50: #eef2ff;
  --brand-100: #e0e7ff;
  --brand-500: #6366f1;
  --brand-600: #4f46e5;
  --brand-700: #4338ca;
  --brand-900: #1e1b4b;

  /* Semantic */
  --color-bg-primary: #ffffff;
  --color-bg-secondary: var(--brand-50);
  --color-bg-inverse: var(--brand-900);
  --color-text-primary: var(--brand-900);
  --color-text-secondary: #64748b;
  --color-text-inverse: #ffffff;
  --color-action-primary: var(--brand-600);
  --color-action-primary-hover: var(--brand-700);
  --color-border-default: #e2e8f0;
  --radius-button: 8px;
  --radius-card: 12px;
}

/* ============================
   Brand B — "Ocean"
   ============================ */
[data-brand="ocean"] {
  /* Primitives */
  --brand-50: #ecfeff;
  --brand-100: #cffafe;
  --brand-500: #06b6d4;
  --brand-600: #0891b2;
  --brand-700: #0e7490;
  --brand-900: #164e63;

  /* Semantic */
  --color-bg-primary: #ffffff;
  --color-bg-secondary: var(--brand-50);
  --color-bg-inverse: var(--brand-900);
  --color-text-primary: var(--brand-900);
  --color-text-secondary: #64748b;
  --color-text-inverse: #ffffff;
  --color-action-primary: var(--brand-600);
  --color-action-primary-hover: var(--brand-700);
  --color-border-default: #e2e8f0;
  --radius-button: 24px;
  --radius-card: 16px;
}

tailwind.config.ts — Token Registration

Register your CSS custom properties in Tailwind so you can use them as utility classes.

import type { Config } from "tailwindcss"

const config: Config = {
  content: [
    "./app/**/*.{js,ts,jsx,tsx}",
    "./components/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {
      colors: {
        bg: {
          primary: "var(--color-bg-primary)",
          secondary: "var(--color-bg-secondary)",
          inverse: "var(--color-bg-inverse)",
        },
        text: {
          primary: "var(--color-text-primary)",
          secondary: "var(--color-text-secondary)",
          inverse: "var(--color-text-inverse)",
        },
        action: {
          primary: "var(--color-action-primary)",
          "primary-hover": "var(--color-action-primary-hover)",
        },
        border: {
          default: "var(--color-border-default)",
        },
      },
      borderRadius: {
        button: "var(--radius-button)",
        card: "var(--radius-card)",
      },
    },
  },
  plugins: [],
}

export default config

Components Use Semantic Tokens

Now your components use the Tailwind utility classes that map to semantic tokens. They automatically adapt to whichever brand is active.

Correct:

<button className="bg-action-primary hover:bg-action-primary-hover text-text-inverse rounded-button">
  Get Started
</button>

Wrong:

{/* Don't do this — hardcoded brand colors */}
<button className="bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg">
  Get Started
</button>

The correct version will automatically change colors when the brand switches. The wrong version will always be indigo, regardless of brand.

The BrandProvider Component

To switch brands dynamically, create a React context that sets the data-brand attribute on the root element.

"use client"

import { createContext, useContext, useEffect, useState } from "react"

type Brand = "midnight" | "ocean"

const BrandContext = createContext<{
  brand: Brand
  setBrand: (brand: Brand) => void
}>({
  brand: "midnight",
  setBrand: () => {},
})

export function BrandProvider({
  children,
  defaultBrand = "midnight",
}: {
  children: React.ReactNode
  defaultBrand?: Brand
}) {
  const [brand, setBrand] = useState<Brand>(defaultBrand)

  useEffect(() => {
    document.documentElement.setAttribute("data-brand", brand)
  }, [brand])

  return (
    <BrandContext.Provider value={{ brand, setBrand }}>
      {children}
    </BrandContext.Provider>
  )
}

export function useBrand() {
  return useContext(BrandContext)
}

// Usage in a component:
// const { brand, setBrand } = useBrand()
// <button onClick={() => setBrand("ocean")}>Switch to Ocean</button>

Typography Scale

Define a consistent typography scale using CSS custom properties and map them to Tailwind classes. This ensures every text element across both brands uses the same hierarchy.

CSS ClassSizeWeightLine HeightTailwind Equivalent
.text-display48px7001.1text-5xl font-bold leading-tight
.text-heading-136px7001.2text-4xl font-bold leading-snug
.text-heading-228px6001.3text-3xl font-semibold leading-snug
.text-heading-322px6001.4text-xl font-semibold leading-normal
.text-body16px4001.6text-base font-normal leading-relaxed
.text-body-sm14px4001.5text-sm font-normal leading-normal
.text-caption12px4001.4text-xs font-normal leading-snug

Documenting Your Token System

Every design system needs documentation. Create a TOKENS.md file at the root of your project that lists all tokens, their purpose, and their values per brand.

# Design Tokens

## Color Tokens

| Token                          | Purpose                | Midnight    | Ocean       |
|--------------------------------|------------------------|-------------|-------------|
| --color-bg-primary             | Main background        | #ffffff     | #ffffff     |
| --color-bg-secondary           | Secondary background   | #eef2ff     | #ecfeff     |
| --color-bg-inverse             | Dark background        | #1e1b4b     | #164e63     |
| --color-text-primary           | Main text color        | #1e1b4b     | #164e63     |
| --color-text-secondary         | Muted text             | #64748b     | #64748b     |
| --color-action-primary         | Buttons, links         | #4f46e5     | #0891b2     |
| --color-action-primary-hover   | Hover state            | #4338ca     | #0e7490     |
| --color-border-default         | Default borders        | #e2e8f0     | #e2e8f0     |

## Radius Tokens

| Token            | Purpose          | Midnight | Ocean |
|------------------|------------------|----------|-------|
| --radius-button  | Button corners   | 8px      | 24px  |
| --radius-card    | Card corners     | 12px     | 16px  |

## Spacing Tokens

| Token     | Value |
|-----------|-------|
| --space-1 | 4px   |
| --space-2 | 8px   |
| --space-4 | 16px  |
| --space-8 | 32px  |

Keep this file updated as you add tokens. It serves as the single source of truth for designers and developers alike.