Brands and themes
A brand (or a theme) is a mode of the collection that holds your semantic tokens. In the brand-ab fixture that collection is Brand(Alias) with the modes BrandA and BrandB; both alias into the same primitives:
{ "Colors": { "Brand": { "Primary": { "$type": "color", "$value": "{Colors.Foundation.Blue.500}" } } }}{ "Colors": { "Brand": { "Primary": { "$type": "color", "$value": "{Colors.Foundation.Navy.500}" } } }}Build one theme per brand
Section titled “Build one theme per brand”List the modes to build in brands:
export default { brands: ['BrandA', 'BrandB'], // …}dta build then writes the primitives once (output/css/variables.css, on :root) and one file per brand, scoped to an attribute with the lower-cased brand name:
[data-brand="brandb"] { --colors-brand-primary: var(--colors-foundation-navy-500); …}Load all of them and switch with the attribute. It works on any element, so a page can show two brands side by side:
<link rel="stylesheet" href="output/css/variables.css" /><link rel="stylesheet" href="output/css/themes/branda.css" /><link rel="stylesheet" href="output/css/themes/brandb.css" />
<html data-brand="branda"> … <section data-brand="brandb">this part uses BrandB</section></html>Because brand values stay var() references to the primitives, a change to Colors.Foundation.Blue.500 reaches every brand that uses it without a rebuild of the themes.
Tokens that alias brand tokens
Section titled “Tokens that alias brand tokens”A collection with one mode (a component tier, say) is written to variables.css on :root. When one of its tokens aliases a brand token, directly or through other single-mode tokens, each theme file declares it again with the same reference. CSS resolves var() where a property is declared, so the copy in the theme is what lets a nested data-brand section resolve it to its own brand. A build of the Learn multi-brand example with a single-mode Component collection added (button.background → {action.background}, button.radius → {shape.control}, card.accent → {button.background}, button.border → {color.blue.600}):
[data-brand="brandb"] { --action-background: var(--color-green-600); --action-foreground: var(--color-neutral-0); --shape-control: var(--radius-999); --type-heading: var(--font-serif); --button-background: var(--action-background); --button-radius: var(--shape-control); --card-accent: var(--button-background);}All four are in variables.css on :root as before; --button-border aliases a primitive only, so it is not repeated in the themes. What base.css writes for them: Outputs.
Breakpoint tokens and brands
Section titled “Breakpoint tokens and brands”The same holds for a breakpoint collection: a breakpoint token that aliases a brand token in any of its modes is repeated in every theme, once per mode, inside the same media queries. The Learn multi-brand example with a Screen collection added (card.radius → {radius.4} on Mobile, {radius.999} on Desktop; card.accent → {color.blue.600} on Mobile, {action.background} on Desktop) and breakpoints: { collection: 'Screen', default: 'Mobile', modes: { Desktop: '(min-width: 1024px)' } }:
[data-brand="brandb"] { --action-background: var(--color-green-600); --action-foreground: var(--color-neutral-0); --shape-control: var(--radius-999); --type-heading: var(--font-serif);}
/* Screen · Mobile (default) */[data-brand="brandb"] { --card-accent: var(--color-blue-600);}
/* Screen · Desktop */@media (min-width: 1024px) { [data-brand="brandb"] { --card-accent: var(--action-background); }}So a <section data-brand="brandb"> inside a BrandA page gets BrandB’s green accent from 1024 px and the blue one below. card.radius reaches primitives only and stays in variables.css.
Light and dark
Section titled “Light and dark”Light and dark are the same mechanism: a collection with the modes Light and Dark (or Graphite), listed in brands. This website does exactly that: its tokens have a Theme collection with Light and Graphite, built into [data-brand="light"] and [data-brand="graphite"], and a small script sets the attribute when you switch the theme.
Brands and themes together
Section titled “Brands and themes together”Per-mode files hold one dimension per collection, so a set with both brands and light/dark (BrandA light, BrandA dark, …) needs one mode per combination there. A DTCG resolver document describes them as two modifiers instead, and dta build combines them. In the dtcg-modifiers fixture a theme (light, dark) and then a brand (a, b) both change accent: the sets give it blue, the dark theme a lighter blue, brand b navy (it comes later in the resolution order, so it wins).
export default { brands: ['a', 'b'], // the brand modifier switches with data-brand; the theme with data-theme // …}Tokens one modifier changes go to that modifier’s theme files as above (themes/theme-dark.css with [data-theme="dark"]). A token both change gets a custom property per layer, and reads the last one that is set:
/* theme × brand: the sets' values and the default contexts; [data-theme] and [data-brand] choose others */:root { --accent-dta-base: var(--color-blue-500); --accent: var(--accent-dta-brand, var(--accent-dta-theme, var(--accent-dta-base)));}/* theme × brand: theme · dark */[data-theme="dark"] { --accent-dta-theme: var(--color-blue-300); --accent: var(--accent-dta-brand, var(--accent-dta-theme, var(--accent-dta-base))); --button-background: var(--accent);}/* theme × brand: brand · b */[data-brand="b"] { --accent-dta-brand: var(--color-navy-700); --accent: var(--accent-dta-brand, var(--accent-dta-theme, var(--accent-dta-base))); --button-background: var(--accent);}:root also holds the layers of each modifier’s default context (none here: light and a leave accent alone), so a page without the attributes gets the default combination. A context that leaves the token alone sets its layer to initial (brand a, light), so a nested section can undo an outer one. Set both attributes on any elements, in either order:
<html data-theme="light" data-brand="a"> <section data-theme="dark">dark × a</section> <section data-brand="b"><div data-theme="dark">dark × b</div></section></html>Each combination gives the value the resolver document gives (a browser test checks all four, nested both ways). dta token get accent lists them, and color-contrast checks a pair in each combination. Two things to know:
- Single-mode tokens that alias such a token (
button.background→{accent}) are declared again in every context, like tokens that alias brand tokens. - A token of one modifier that aliases it (
ring→{accent}in the dark theme) is declared again inside each of that modifier’s contexts, wherever an element sets the other modifier, with the single-mode tokens that alias it (button.focus→{ring}).@scopelets the nearest context win (a browser without@scopeignores the block, and such a nested section keeps the outer value):
/* theme × brand: theme · dark tokens again where [data-brand] is set inside */@scope ([data-theme="dark"]) { :scope[data-brand], [data-brand] { --ring: var(--accent); /** Focus ring */ --button-focus: var(--ring); }}- Tailwind and JS outputs leave combined tokens out; use the CSS custom properties. A token that aliases one is written resolved there for the defaults of the other modifiers (the Tailwind file of the dark theme has
ringfor dark × a).
Keep the brand layer clean
Section titled “Keep the brand layer clean”The semantic-must-alias rule fails when a token in the brand layer holds a raw value instead of an alias, so a brand never drifts from the palette:
lint: { rules: { 'semantic-must-alias': { severity: 'error', collections: ['Brand(Alias)'] } } }color-contrast checks text and background pairs through their aliases, once per brand (and per light or dark mode, if you have them); a finding names the brand it fails in: Lint rules.