Tailwind CSS Multi-Brand Theming: One Codebase, Many Brands
AI generated
</>
tw
Tailwind CSS · Multi-Brand · Design Tokens · CSS Custom Properties
Tailwind CSS Multi-Brand Theming
one codebase, many brands with @theme and custom properties

White-label products, multi-tenant SaaS and agencies with many clients face the same challenge: how do you serve multiple brands from a single codebase without duplicating class names and without complicating the build process? Tailwind CSS multi-brand theming with @theme and CSS custom properties is the answer.

14 min read @theme · CSS Custom Properties · data attributes · Theme Switching Tailwind v4 · White-Label · Multi-Tenant

1. The core problem in multi-brand projects

Anyone building for multiple brands from a shared codebase knows the classic trap: colors and typography start out hard-coded, text-blue-600 for brand A, text-red-600 for brand B. With every new brand, the number of conditional classes, if constructs in templates and separate CSS files grows. The result is a codebase that is hard to maintain, even harder to test, and grows exponentially more complex with every new brand. Tailwind CSS multi-brand theming solves this problem through a clean separation between the design token layer and the utility layer.

The core idea is simple: instead of using brand-specific colors directly as Tailwind classes, semantic tokens are defined, --color-brand-primary, --color-brand-secondary, --font-brand. Tailwind classes such as bg-brand-primary and text-brand-secondary reference these tokens. When the brand changes, only the CSS custom property values behind the tokens change, the classes in the templates stay unchanged. That is the fundamental advantage of multi-brand theming over hard class switching.

2. Design tokens as the foundation

Design tokens are the smallest unit of a design system. In a multi-brand theming system, they are the only place where brand information is stored. Every other part of the system, components, layouts, typography, references tokens exclusively, never direct color values. The token hierarchy in Tailwind CSS multi-brand theming typically has three levels: primitive tokens (actual values like #0ea5e9), semantic tokens (meaning, like --color-action) and component tokens (context, like --btn-primary-bg).

For a real multi-brand theming system built with Tailwind, semantic tokens are sufficient in most cases. Component tokens are only introduced when a component deviates so strongly per brand that semantic tokens are not enough. The token system should be defined in its own CSS file (e.g. tokens.css) that holds all brand overrides. This file is generated by the CI/CD pipeline from a central token source (e.g. a design token JSON from the Figma Tokens plugin).


/* tokens.css, generated from design token source of truth */

/* === Primitive tokens (shared across all brands) === */
:root {
  --primitive-sky-500: #0ea5e9;
  --primitive-sky-700: #0369a1;
  --primitive-emerald-500: #10b981;
  --primitive-rose-500: #f43f5e;
  --primitive-slate-900: #0f172a;
}

/* === Brand A: Mironsoft (default) === */
[data-brand="mironsoft"], :root {
  --color-brand-primary: var(--primitive-sky-500);
  --color-brand-primary-dark: var(--primitive-sky-700);
  --color-brand-success: var(--primitive-emerald-500);
  --color-brand-danger: var(--primitive-rose-500);
  --font-brand-heading: 'Inter', sans-serif;
  --radius-brand-card: 1rem;
}

/* === Brand B: Acme Corp === */
[data-brand="acme"] {
  --color-brand-primary: #7c3aed;
  --color-brand-primary-dark: #5b21b6;
  --color-brand-success: #059669;
  --color-brand-danger: #dc2626;
  --font-brand-heading: 'Poppins', sans-serif;
  --radius-brand-card: 0.5rem;
}

/* === Brand C: Greentech === */
[data-brand="greentech"] {
  --color-brand-primary: #16a34a;
  --color-brand-primary-dark: #15803d;
  --color-brand-success: #0d9488;
  --color-brand-danger: #b45309;
  --font-brand-heading: 'Nunito', sans-serif;
  --radius-brand-card: 1.5rem;
}

3. @theme in Tailwind CSS v4 for brand tokens

In Tailwind CSS v4, design tokens are connected to the Tailwind utility system through the @theme directive. The trick for multi-brand theming: @theme references CSS custom properties that are defined in the token file. Tailwind generates utility classes from these references. When the custom properties change through a theme selector, the values of every utility that references those properties change automatically, without a rebuild being necessary.

This enables runtime theme switching: the user selects a brand, a data-brand attribute is set, CSS custom properties switch instantly, and all Tailwind classes show the new brand. This pattern requires that every brand-specific token is defined as a CSS custom property and that Tailwind's @theme only points to those properties. The utilities bg-brand-primary, text-brand-primary and ring-brand-primary exist only once in the CSS, but show the correct values for every brand.

4. CSS custom properties as the theming layer

CSS custom properties are the foundation of any serious Tailwind CSS multi-brand theming system. They are the only native CSS technique that allows values to change at runtime without reloading or recalculating CSS. Every CSS custom property can be overridden by a selector with higher specificity, and that is exactly what the multi-brand theming pattern uses. The root selector defines the default brand, and every brand selector overrides the tokens.

The important difference from Tailwind classes: custom properties cascade. A --color-brand-primary on a parent element applies to all child elements until it is overridden. That makes it possible to show multiple brands within a single page, for example in an editor's preview view or in a comparison dashboard. For this use case, class-based theming systems would need complex logic, whereas custom properties support this by nature.

5. Data attributes for theme switching

The data-brand attribute on the html or body element is the cleanest method for multi-brand theming. It makes the active brand readable in the HTML markup, allows CSS selectors like [data-brand="acme"], and avoids class collisions with Tailwind. Switching in JavaScript is a single attribute-set call that immediately updates all CSS custom properties and therefore all Tailwind utilities. No re-render, no state management, no repeated API call.

In Magento 2 with Hyvä Themes, the data-brand attribute is ideally rendered server-side: the PHP template reads the brand from the store configuration and sets the attribute at page render time. For dynamic switching without a page reload, e.g. in a configurator, Alpine.js takes over setting the attribute. The multi-brand theming system stays functional regardless, because all styling logic lives in CSS custom properties and no JavaScript is needed to render the correct colors.


<!-- Server-rendered: brand set on html element -->
<html lang="en" data-brand="acme">

<!-- Alpine.js brand switcher component -->
<div
  x-data="{
    currentBrand: localStorage.getItem('brand') || 'mironsoft',
    brands: ['mironsoft', 'acme', 'greentech'],
    switchBrand(brand) {
      this.currentBrand = brand;
      document.documentElement.setAttribute('data-brand', brand);
      localStorage.setItem('brand', brand);
    },
    init() {
      // Apply stored brand on page load
      document.documentElement.setAttribute('data-brand', this.currentBrand);
    }
  }"
  class="flex gap-2"
>
  <template x-for="brand in brands" :key="brand">
    <button
      @click="switchBrand(brand)"
      :class="currentBrand === brand
        ? 'bg-brand-primary text-white'
        : 'bg-white text-slate-700 border border-slate-200'"
      class="px-4 py-2 rounded-lg text-sm font-semibold transition-colors"
      x-text="brand"
    ></button>
  </template>
</div>

<!-- Tailwind classes using brand tokens: same classes, different values per brand -->
<section class="bg-brand-primary text-white p-8 rounded-[--radius-brand-card]">
  <h1 class="font-bold text-2xl">Brand headline</h1>
  <button class="bg-white text-brand-primary px-6 py-3 rounded-lg font-semibold mt-4">
    Call to action
  </button>
</section>

6. Dark mode in a multi-brand context

Having to combine dark mode with multi-brand theming is the master discipline of Tailwind theming. The naive solution, writing a selector for every brand times dark-mode combination, produces quadratically growing CSS complexity. The elegant pattern for multi-brand theming with dark mode uses two combined attribute selectors: [data-brand="acme"][data-theme="dark"] overrides only the tokens that should look different in dark mode, while all other tokens are inherited from the brand definition.

Tailwind CSS v4 supports dark mode via the dark: prefix, which reacts to the CSS class .dark or, in v4, to a configurable attribute. For multi-brand theming, combining data-brand for brands with data-theme="dark" for dark mode is recommended, because it lets both dimensions be toggled independently. The CSS custom properties for dark mode override only the visual tokens (colors, backgrounds), while structural tokens (radii, typography) stay brand-specific.

7. Build pipeline for multiple brands

In Tailwind v3 it was common to run a separate build for every brand with its own tailwind.config.js. That produced multiple CSS files with duplicated utility classes and only differing values. In Tailwind v4 with CSS custom properties, this approach is eliminated entirely: there is a single CSS output file that contains all brand tokens. The build runs once, and the result serves every brand.

For very large multi-brand theming projects with hundreds of brands, a token generation script is recommended that creates a single CSS file with all brand selectors from a JSON source file. This JSON file is the only place edited when a new brand is added. The build script reads the JSON, generates the CSS custom property blocks and writes them into the token CSS file. Tailwind processes this file transparently, it "knows" nothing about the brands, it only knows the utility classes generated from @theme.

8. Approaches compared

There are several ways to implement multi-brand theming in Tailwind. The table below shows the main approaches and their suitability for real projects.

Approach How it works Advantages Disadvantages
Separate configs (v3) One tailwind.config.js per brand, separate builds Full separation Multiple builds, CSS duplication
CSS custom properties One build, tokens switchable via data attribute Runtime switching, one build Only value tokens, no class changes
Class override Brand classes set in templates via condition Easy to understand Grows exponentially with brand count
@theme + custom properties Tailwind v4: tokens in @theme, values via CSS props Best of both worlds Requires Tailwind v4

9. Theme testing and visual regression tests

A multi-brand theming system is only as good as its tests. Visual regression tests with Playwright or Cypress can create separate screenshot snapshots for every brand and compare them on pull requests. The pattern: for every test, the data-brand attribute is set programmatically, a screenshot is taken and compared against the stored baseline screenshot. When a design token changes, all tests for the brands that use that token fail, that is intended behavior.

For Storybook-based component libraries, multi-brand theming enables an elegant story structure: one story, multiple brand decorators. Each decorator sets data-brand on the wrapper element. Chromatic renders every variant and compares them. This replaces dozens of duplicated stories with a single story that has multiple brand variants. The effort of adding a new brand is reduced to creating the token CSS and adding the brand to the decorator list.


/* main.css, single file for all brands in Tailwind v4 */
@import "tailwindcss";
@import "./tokens.css";

@theme {
  /* Map CSS Custom Properties to Tailwind utilities */
  --color-brand-primary: var(--color-brand-primary);
  --color-brand-primary-dark: var(--color-brand-primary-dark);
  --color-brand-success: var(--color-brand-success);
  --color-brand-danger: var(--color-brand-danger);

  /* Structural tokens: brand-specific radius and typography */
  --radius-brand-card: var(--radius-brand-card);
  --font-family-brand: var(--font-brand-heading);
}

/* Utility classes for dark mode, works across all brands */
@utility bg-brand-surface {
  background-color: var(--color-brand-primary);

  @media (prefers-color-scheme: dark) {
    background-color: var(--color-brand-primary-dark);
  }
}

/* Brand-aware focus ring, changes color per active brand */
@utility focus-brand {
  &:focus-visible {
    outline: 2px solid var(--color-brand-primary);
    outline-offset: 2px;
  }
}

10. Summary

Tailwind CSS multi-brand theming with CSS custom properties and Tailwind v4 is the most robust pattern for projects that need to serve multiple brands from one codebase. The architecture is clear: semantic design tokens as CSS custom properties, @theme to connect them to Tailwind utilities, data-brand attributes for switching. A single CSS output file serves every brand. Runtime switching without a page reload is possible. New brands are introduced by adding a token block, without template changes.

The most important principle in implementation: never use direct color values in Tailwind classes once a project has more than one brand. Always use semantic token classes like bg-brand-primary instead of bg-sky-500. Following this discipline consistently pays off with every new brand and keeps the multi-brand theming system maintainable, testable and extensible.

Tailwind CSS Multi-Brand Theming: the essentials at a glance

Token architecture

Semantic design tokens as CSS custom properties. Primitive → semantic → component. @theme connects tokens to Tailwind utilities.

Theme switching

data-brand attribute on the html element. CSS selectors override tokens. Runtime switching without page reload and without rebuild.

Build efficiency

A single Tailwind build for all brands. No CSS duplication. New brands via a token block, no template changes needed.

Testing

Playwright/Cypress set data-brand programmatically. Screenshots for every brand. One story, multiple brand decorators in Storybook.