Brik Design System
Theming

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:

  • ThemeProvider adds theme-brand-brik to <body> and sets data-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 into dist/tokens.css); the client-sim block lives in tokens/font-audit.css.

See tokens/CASCADE.md for the full load order.

Built-in themes

SelectorDescription
.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-simFont 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.

SiteAttributeScopes
Vale Partnersdata-audiencehealthcare, land, commercial
brikdesigns.comdata-servicemarketing, back-office, product
Healthcare networkdata-departmentcardiology, 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 groupPer-client override?Notes
Brand colors (--background-brand-primary, --text-brand-primary, --surface-brand-primary, --border-brand-primary)RequiredCore brand identity.
Interaction states (--background-brand-primary-hover, --background-brand-primary-pressed)Required if brand colors changeDefined 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)RecommendedTune neutrals to the brand.
Fonts (--font-family-heading, --font-family-body, --font-family-label)OptionalSee "Typography-only client override" below.
Canonical status (--background/surface/text/border-positive/negative/warning)YesFirst-class Figma variables. Override if the brand red/green/yellow clashes with the default.
Extended status (--background-status-info/neutral/purple/orange)NoGap-fills using system colors with universal semantic meaning.
Presence (--background-presence-online/away/busy/offline)No — intentionally frozenShared chat/status semantics across products.
Shadows + overlay (--shadow-sm..--shadow-xl, --background-overlay)NoHardcoded 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.

On this page