Deep Dive

Multi-Brand Theming

Modern design systems serve multiple products, brands, and platforms. The two-tier token architecture makes this possible without duplicating the entire system.

Runtime brand switching via data attributes
Six dimensions: color, shape, spacing, motion, type, density
CSS cascade layers for predictable overrides
Semantic tokens as the theming surface
Ten brand themes with distinct personalities

The Theming Model

Multi-brand theming works through three layers. Core tokens define raw primitives (palettes, scales). Semantic tokens give those primitives meaning (accent, background, border). Brand tokens remap semantic values to different core primitives.

Semantic Token                       Default (red)         Corporate (blue)
─────────────────                    ─────────────         ────────────────
--semantic-color-foreground-accent   → palette.red.500     → palette.blue.500
--semantic-color-background-accent   → palette.red.500     → palette.blue.500
--semantic-color-border-accent       → palette.red.500     → palette.blue.500
--semantic-color-foreground-link     → palette.red.500     → palette.blue.600
--semantic-shape-control-radius      → shape.radius.04     → shape.radius.02
--semantic-spacing-component-padding → spacing.size.05     → spacing.size.04
--semantic-motion-interaction-dur.   → motion.dur.short    → motion.dur.short2

Components always reference --semantic-* tokens.
Brand files remap which core primitives those tokens resolve to.

The source files mirror this structure. Core token files define the shared primitives, and each brand file selectively overrides the semantic mappings:

Token Architecture
──────────────────────────────────────────────────────────────────
core/                    semantic/                brands/
├── color.tokens.json    ├── color.tokens.json    ├── default.tokens.json
├── spacing.tokens.json  ├── spacing.tokens.json  ├── corporate.tokens.json
├── shape.tokens.json    ├── shape.tokens.json    ├── forest.tokens.json
├── motion.tokens.json   ├── motion.tokens.json   ├── sunset.tokens.json
├── typography.tokens.json└── typography.tokens.json├── midnight.tokens.json
├── elevation.tokens.json                         ├── ocean.tokens.json
└── density.tokens.json                           ├── canary.tokens.json
                                                  ├── monochrome.tokens.json
density/                                          ├── rose.tokens.json
├── tight.tokens.json                             └── slate.tokens.json
├── compact.tokens.json
├── default.tokens.json
└── spacious.tokens.json

Components reference semantic tokens. Brand and density layers override
semantic values when [data-brand] and [data-density] attributes are set.

What Changes Per Brand

Each brand can customize six categories of tokens:

  • Color — Primary accent, link colors, highlights, action states
  • Shape — Border radius (none, sharp, medium, rounded, pill), card borders, elevation
  • Spacing — Component padding, gaps, and card spacing
  • Motion — Transition duration and easing curves
  • Typography — Font weights for body and headings
  • Density — Spacing scale multipliers (tight, compact, default, spacious)

Brand Token Structure

Each brand token file references core primitives using the DTCG token reference syntax. The $extensions object provides separate light and dark values for color tokens, enabling per-brand dark mode adjustments:

// brands/corporate.tokens.json
{
  "$brand": {
    "name": "corporate",
    "description": "Professional corporate brand with blue accents",
    "accent": "blue",
    "density": "compact"
  },
  "color": {
    "foreground": {
      "accent": {
        "$type": "color",
        "$value": "{color.palette.blue.500}",
        "$extensions": {
          "design.paths.light": "{color.palette.blue.500}",
          "design.paths.dark": "{color.palette.blue.400}"
        }
      }
    },
    "background": {
      "accent": {
        "$type": "color",
        "$value": "{color.palette.blue.500}",
        "$extensions": {
          "design.paths.light": "{color.palette.blue.500}",
          "design.paths.dark": "{color.palette.blue.400}"
        }
      }
    }
  },
  "shape": {
    "control": {
      "radius": {
        "default": { "$value": "{shape.radius.01}" } // 2px - sharp
      }
    },
    "card": {
      "radius": { "$value": "{shape.radius.01}" },
      "elevation": {
        "default": { "$value": "{elevation.level.1}" }
      }
    }
  },
  "spacing": {
    "component": {
      "padding": { "$value": "{spacing.size.04}" }, // 8px - compact
      "gap": { "$value": "{spacing.size.03}" }      // 4px
    },
    "card": {
      "padding": { "$value": "{spacing.size.05}" } // 12px
    }
  },
  "motion": {
    "interaction": {
      "duration": { "$value": "{motion.duration.short2}" } // 83ms
    }
  },
  "typography": {
    "body": { "fontWeight": { "$value": "{typography.weight.medium}" } },
    "heading": { "fontWeight": { "$value": "{typography.weight.bold}" } }
  }
}

CSS Output with Cascade Layers

The build system generates CSS with cascade layers. Each layer has increasing precedence—brand overrides always win over theme defaults, and theme defaults win over semantic defaults:

@layer core, semantic, theme, brand, density;

@layer theme {
  /* Light/dark theme defaults (applied via class on <html>) */
  .light {
    --semantic-color-foreground-accent: #d9292b;   /* default red */
    --semantic-color-background-primary: #ffffff;
    /* ...all semantic color tokens for light mode... */
  }
  .dark { /* dark mode overrides */ }

  @media (prefers-color-scheme: dark) {
    :root { /* system dark mode */ }
    .light { /* manual light override when system prefers dark */ }
  }
}

@layer brand {
  /* Base brand overrides (light mode default) */
  [data-brand="corporate"] {
    --semantic-color-foreground-accent: var(--core-color-palette-blue-500);
    --semantic-color-background-accent: var(--core-color-palette-blue-500);
    --semantic-shape-control-radius-default: var(--core-shape-radius-01);
    --semantic-shape-card-radius: var(--core-shape-radius-01);
    --semantic-spacing-component-padding: var(--core-spacing-size-04);
    --semantic-motion-interaction-duration: var(--core-motion-duration-short2);
    --semantic-typography-body-font-weight: var(--core-typography-weight-medium);
  }

  /* Dark mode overrides for this brand */
  @media (prefers-color-scheme: dark) {
    [data-brand="corporate"] {
      --semantic-color-foreground-accent: var(--core-color-palette-blue-400);
    }
  }
}

@layer density {
  [data-density="compact"] {
    --semantic-spacing-padding-container: var(--semantic-spacing-density-compact-lg);
    --semantic-spacing-padding-card: var(--semantic-spacing-density-compact-sm);
    --semantic-spacing-gap-grid: var(--semantic-spacing-density-compact-sm);
  }
}

Runtime Brand Switching

Brand and density switching at runtime is handled by updating data attributes on the document element. The BrandContext manages state and persistence:

// BrandContext manages brand, density, and font preferences
type BrandId = 'default' | 'corporate' | 'forest' | 'sunset' |
               'midnight' | 'ocean' | 'canary' | 'monochrome' |
               'rose' | 'slate';

type DensityId = 'tight' | 'compact' | 'default' | 'spacious';

function setBrand(brand: BrandId) {
  document.documentElement.setAttribute('data-brand', brand);
  localStorage.setItem('brand', brand);
}

function setDensity(density: DensityId) {
  document.documentElement.setAttribute('data-density', density);
  localStorage.setItem('density', density);
}

// Usage - instantly switches all tokens
setBrand('corporate');
setDensity('compact');

Core Layer: Brand-Agnostic Primitives

The core layer contains primitives that all brands share. These are the raw materials—color palettes, spacing scales, typography ramps—that brands draw from. No brand defines its own palette; each one references values from this shared set.

// core/color.tokens.json - Universal palette
{
  "palette": {
    "red": { "500": { "$type": "color", "$value": "#d9292b" } },
    "blue": { "500": { "$type": "color", "$value": "#0a65fe" } },
    "green": { "500": { "$type": "color", "$value": "#22c55e" } },
    "teal": { "500": { "$type": "color", "$value": "#14b8a6" } },
    "purple": { "500": { "$type": "color", "$value": "#a855f7" } },
    "yellow": { "500": { "$type": "color", "$value": "#eab308" } },
    "neutral": { "500": { "$type": "color", "$value": "#737373" } }
  }
}

// core/shape.tokens.json - Radius scale
{
  "radius": {
    "none": { "$value": "0px" },   // No radius
    "01": { "$value": "2px" },     // Sharp
    "02": { "$value": "4px" },     // Small
    "medium": { "$value": "6px" }, // Medium-small
    "03": { "$value": "8px" },     // Medium
    "04": { "$value": "16px" },    // Large
    "05": { "$value": "32px" },    // Extra large
    "full": { "$value": "9999px" } // Pill
  }
}

// core/density.tokens.json - Spacing density scales
{
  "density": {
    "tight": { "sm": "4px", "md": "6px", "lg": "8px" },
    "compact": { "sm": "8px", "md": "12px", "lg": "16px" },
    "default": { "sm": "12px", "md": "16px", "lg": "24px" },
    "spacious": { "sm": "16px", "md": "24px", "lg": "32px" }
  }
}

Pitfalls to Avoid

1. Duplicating Core Tokens Per Brand

Each brand should reference core tokens, not define their own values. Duplicating defeats the purpose of the two-tier model.

// BAD: Brand defines its own primitives
{ "blue": { "500": { "$value": "#0a65fe" } } } // Duplicates core!

// GOOD: Brand references core primitives
{ "background": { "brand": { "$value": "{core.color.palette.blue.500}" } } }

2. Inconsistent Semantic Structure

All brands must use the same semantic token structure. Components reference semantic paths, so those paths must exist in every brand.

3. Hardcoding Brand Logic in Components

Components should be brand-agnostic. They reference semantic tokens and let the token system handle brand differences.

// BAD: Brand logic in component
.button { background: var(--brand-a-blue); }

// GOOD: Brand-agnostic component
.button { background: var(--semantic-color-background-accent); }

4. Forgetting Accessibility Across Brands

Each brand's semantic tokens must maintain accessibility requirements. A brand can't choose colors that fail contrast ratios.

Summary

  • Core tokens are brand-agnostic — Shared palettes, scales, and ramps
  • Semantic tokens are the theming surface — Where brands diverge
  • Six dimensions of variation — Color, shape, spacing, motion, typography, density
  • CSS cascade layers — Ensure brand and density overrides take precedence
  • Runtime switching via data attributes — Instant brand and density changes
  • Ten brand themes — Each with distinct color, shape, and motion personalities

Next Steps

Multi-brand theming relies on the resolver module for complex resolution scenarios. For simpler setups, understand how build outputs generate brand-specific artifacts, and how accessibility tokens ensure all brands remain inclusive.