Client Themes
The Tokens Theming Dimension — per-client semantic token overrides. Override matrix, minimal theme-{client}.css template, and interaction-state rules.
BDS ships 3 built-in themes. Each theme overrides a curated set of semantic color and typography tokens while leaving primitives and spacing untouched. The 8 numbered website template themes (theme-1 through theme-8) have moved to the brik/brik-website-themes repo and are not part of the client project architecture.
How themes work
Brand themes use a class on <body> plus the data-theme attribute on <html>. The ThemeProvider component manages both automatically.
// In your layout or app root:
<ThemeProvider initialTheme={{ themeNumber: 'brik-dark' }}>
<App />
</ThemeProvider>Under the hood:
ThemeProvideraddstheme-brand-brikto<body>and setsdata-theme="dark"(or"light") on<html>.- CSS selectors like
.theme-brand-brik[data-theme="dark"] { … }override the semantic tokens for that theme. - Brik's theme blocks live in
tokens/theme-brand-brik.css(bundled intodist/tokens.css); the client-sim block lives intokens/font-audit.css.
See tokens/CASCADE.md for the full load order.
Built-in themes
| Selector | Description |
|---|---|
.theme-brand-brik[data-theme="light"] (or no data-theme) | Brik brand — poppy red, Poppins on white |
.theme-brand-brik[data-theme="dark"] | Brik brand, dark mode — poppy red on near-black |
.theme-brand-brik.theme-client-sim | Font audit tool — Georgia / Verdana / Courier New exposes semantic font-family misuse |
Use the paintbrush icon in the Storybook toolbar to switch themes in real time.
Token cascade
The full cascade order in a consuming project's globals.css (order matters):
/* 1. Generated tokens (light + dark) plus Brik defaults */
@import '@brikdesigns/bds/tokens.css';
/* 2. Client brand overrides */
@import './styles/theme-{client}.css';
/* 3. Component CSS bundle */
@import '@brikdesigns/bds/styles.css';tokens.css bundles figma-tokens + figma-tokens-dark + figma-dark-corrections + theme-brand-brik + animations + motion-classes. Style Dictionary regenerates the figma layer; never edit those files manually. gap-fills.css (also bundled) holds interaction states, status tokens, and other semantics not yet promoted to Figma.
What each theme overrides
Each .body.theme-{name} block overrides a fixed set of semantic color tokens — the same tokens documented in Primitives → Color. These include:
--background-brand-primary,--background-brand-secondary--text-primary,--text-secondary,--text-muted,--text-brand-primary--surface-primary,--surface-secondary,--surface-navigation,--surface-brand-primary--border-primary,--border-secondary,--border-brand-primary--page-primary,--page-brand-primary
--surface-nav is a deprecated alias for --surface-navigation. Both resolve to the same value. Prefer --surface-navigation in new client overrides.
Typography tokens are not overridden by data-theme="light" vs data-theme="dark" — both share the Brik font stack. The theme-client-sim class demonstrates font family overrides only.
Spacing tokens are not theme-controlled — spacing comes from the Base/Spacious mode set on .body, not from theme classes.
Adding a custom client brand
Create a theme-{client}.css file in the consuming project and add it as the final import in globals.css (after the BDS cascade above). Override any semantic tokens at :root level:
/* theme-acme.css — all that's needed for a full rebrand */
:root {
/* Required — brand identity */
--background-brand-primary: #2563EB;
--text-brand-primary: #2563EB;
--border-brand-primary: #2563EB;
--surface-brand-primary: #EFF6FF;
/* Required if overriding brand-primary — interaction states */
--background-brand-primary-hover: #1D4ED8;
--background-brand-primary-pressed: #1E40AF;
/* Recommended — surfaces and page */
--page-primary: #F8FAFC;
--surface-primary: #FFFFFF;
--surface-secondary: #F1F5F9;
/* Optional — custom fonts */
--font-family-heading: 'Neue Haas Grotesk', sans-serif;
--font-family-body: 'Inter', sans-serif;
--font-family-label: 'Inter', sans-serif;
}No .body.theme-{name} class needed — the :root block wins via cascade order.
Per-audience (or service-line) scope binding
Some client sites carry multiple brand colors at once — Vale Partners has three audience verticals (Healthcare, Land, Commercial), each with its own brand hue; brikdesigns.com has service lines (Marketing, Back-Office, Product) that each need a different accent. The pattern below binds canonical brand tokens once per scope so components keep referencing the same canonical names while the page expresses N brand colors simultaneously.
The pattern
Re-bind --background-brand-primary, --text-brand-primary, --border-brand-primary (and their hover/pressed states) inside a CSS attribute selector. Components stay scope-blind — they keep reading the canonical token. Each subtree the attribute selects gets its own value resolved.
/* theme-vale.css — primitives, then canonical bindings, then scoped overrides */
:root {
/* Brand primitives — one ramp per client hue, follows the Figma 6-stop rail
(lightest, lighter, light, dark, darker, darkest). Documented in
/docs/primitives/color → "Tonal scale + interaction state". */
--vale-partners-olive: #698339;
--vale-partners-olive-lightest: #e8edd9;
--vale-partners-olive-lighter: #c2d09a;
--vale-partners-olive-light: #a3b66f;
--vale-partners-olive-dark: #4f6429;
--vale-partners-olive-darker: #3d4c23;
--vale-partners-olive-darkest: #2a3318;
--vale-partners-gold: #c49a2f;
--vale-partners-gold-lightest: #f4ecd2;
/* …rest of the gold ramp… */
--vale-partners-navy: #95afd4;
--vale-partners-navy-darker: #1c3252;
/* …rest of the navy ramp… */
/* Default brand binding (no audience selected) — components fall back here. */
--background-brand-primary: var(--vale-partners-navy-darker);
--text-brand-primary: var(--vale-partners-navy-darker);
--border-brand-primary: var(--vale-partners-navy-darker);
}
/* Audience scopes — each one re-binds the canonical brand tokens inside its
subtree. Adding a fourth audience is one more block; nothing else changes. */
[data-audience='healthcare'] {
--background-brand-primary: var(--vale-partners-olive);
--text-brand-primary: var(--vale-partners-olive-darker);
--border-brand-primary: var(--vale-partners-olive);
--background-brand-primary-hover: var(--vale-partners-olive-dark);
--background-brand-primary-pressed: var(--vale-partners-olive-darker);
}
[data-audience='land'] {
--background-brand-primary: var(--vale-partners-gold);
--text-brand-primary: var(--vale-partners-gold);
--border-brand-primary: var(--vale-partners-gold);
--background-brand-primary-hover: var(--vale-partners-gold-dark);
--background-brand-primary-pressed: var(--vale-partners-gold-darker);
}
[data-audience='commercial'] {
--background-brand-primary: var(--vale-partners-navy-darker);
--text-brand-primary: var(--vale-partners-navy-darker);
--border-brand-primary: var(--vale-partners-navy-darker);
--background-brand-primary-hover: var(--vale-partners-navy-dark);
--background-brand-primary-pressed: var(--vale-partners-navy-darkest);
}Why the same page can show all three audiences at once
CSS custom properties scope to the element they're set on and cascade into every descendant. Three audience blocks side-by-side on a homepage are three independent subtrees — each resolves --background-brand-primary to its own value:
<section class="services-grid">
<article data-audience="healthcare">
<h3 style="color: var(--text-brand-primary)">Healthcare</h3> <!-- olive -->
<button style="background: var(--background-brand-primary)">Explore</button>
</article>
<article data-audience="land">
<h3 style="color: var(--text-brand-primary)">Land</h3> <!-- gold -->
<button style="background: var(--background-brand-primary)">Explore</button>
</article>
<article data-audience="commercial">
<h3 style="color: var(--text-brand-primary)">Commercial</h3> <!-- navy -->
<button style="background: var(--background-brand-primary)">Explore</button>
</article>
</section>Same component, three brand identities, one canonical token. No conditional rendering, no per-audience component variants. The whole page (<html data-audience="commercial">) or one column (<article data-audience="commercial">) can carry the scope — the cascade handles both.
Choosing the attribute name
Use a name that matches the semantic axis the site is organized by. The mechanism is identical; only the vocabulary differs.
| Site | Attribute | Scopes |
|---|---|---|
| Vale Partners | data-audience | healthcare, land, commercial |
| brikdesigns.com | data-service | marketing, back-office, product |
| Healthcare network | data-department | cardiology, pediatrics, oncology |
Pick one per site and document it in the client's theme-{client}.css. Don't mix attribute names within a single site — it makes the cascade harder to predict.
Connection to BCS industry packs
Industry packs in @brikdesigns/bds/content-system express the semantic vocabulary, not the colors. Categories that drive audience pathways carry an audienceId:
// real-estate-commercial.ts (excerpt)
servicesMegaMenu: {
categories: [
{ audienceId: 'healthcare', heading: 'Healthcare', icon: 'ph:stethoscope', items: [...] },
{ audienceId: 'land', heading: 'Land', icon: 'ph:tree', items: [...] },
{ audienceId: 'commercial', heading: 'Commercial', icon: 'ph:storefront', items: [...] },
],
}The blueprint that renders this menu binds each audienceId to a [data-audience=X] attribute on the rendered column; the client theme above does the rest. Pack data carries no color decisions — visual binding is exclusively a client-theme concern. Sites without per-audience theming (the default) render every column in the same canonical brand color; content structure stays intact, no visual loss.
Don't add --background-{role}-primary (e.g. --background-land-primary) to canonical. Canonical is brand-agnostic and stays small. Audience semantics belong in the scope selector, not in the token name. The pattern above is what gives the system its scale property — adding a client or audience is one CSS block, never a new canonical token.
Overridability matrix
Not every token is meant to vary per brand. Use the table to decide what to put in a client theme-{client}.css.
| Token group | Per-client override? | Notes |
|---|---|---|
Brand colors (--background-brand-primary, --text-brand-primary, --surface-brand-primary, --border-brand-primary) | Required | Core brand identity. |
Interaction states (--background-brand-primary-hover, --background-brand-primary-pressed) | Required if brand colors change | Defined at :root in gap-fills.css — not inside theme blocks. If you change brand without overriding these, hover/press keep the default Poppy tint. |
Surfaces + page (--surface-primary, --surface-secondary, --page-primary) | Recommended | Tune neutrals to the brand. |
Fonts (--font-family-heading, --font-family-body, --font-family-label) | Optional | See "Typography-only client override" below. |
Canonical status (--background/surface/text/border-positive/negative/warning) | Yes | First-class Figma variables. Override if the brand red/green/yellow clashes with the default. |
Extended status (--background-status-info/neutral/purple/orange) | No | Gap-fills using system colors with universal semantic meaning. |
Presence (--background-presence-online/away/busy/offline) | No — intentionally frozen | Shared chat/status semantics across products. |
Shadows + overlay (--shadow-sm..--shadow-xl, --background-overlay) | No | Hardcoded rgba() values; not expected to vary per brand. |
Interaction-state override example
:root {
--background-brand-primary: #2563EB;
--background-brand-primary-hover: #1D4ED8; /* must set manually */
--background-brand-primary-pressed: #1E40AF; /* must set manually */
}Status token naming
Canonical names are --background/surface/text/border-positive/negative/warning (Figma-aligned). The older --*-status-* names (--background-status-error, --text-status-success, etc.) are deprecated backward-compat aliases — migrate to the canonical names.
Typography-only client override
To override fonts without changing any colors:
:root {
--font-family-heading: 'Playfair Display', serif;
--font-family-body: 'Source Sans 3', sans-serif;
--font-family-label: 'Source Sans 3', sans-serif;
--font-family-display: 'Playfair Display', serif;
}This is exactly what .body.theme-client-sim demonstrates in the toolbar.
Related
- Theming Overview — the four-layer cascade in context
- Atmospheres — the Atmospheres Dimension (CSS decoration overlays)
- Primitives → Color — the semantic tokens this layer overrides
Theming
Compose per-client brand identity across four orthogonal Theming Dimensions — Tokens, Atmospheres, Layout, Blueprints.
Atmospheres
The Atmospheres Theming Dimension — CSS decoration overlays that establish visual character (editorial-luxury, cinematic, warm-soft, and more) without duplicating CSS per client.