refract v0.1 · beta
Cookbook / White-labeling

White-labeling

One product, N brands. Author one base theme, then derive each brand as an override() child. Every brand shares the base's recipe structure, so the emitted class names are identical — only the variable values differ. Switching brands is a stylesheet swap; your markup never changes.

1 · One base, many brands

Author the shared structure once. Each brand overrides only what differs — usually a brand colour or two; the tonal steps re-synthesize from the new base for free.

brands.ts
const base = createTheme(raw, { adapter: createCssAdapter() });

const acme   = base.override({ colors: { brand: { base: "#e8590c", text: "#ffffff" } } });
const globex = base.override({ colors: { brand: { base: "#2f9e44", text: "#ffffff" } } });
Extending a theme you don't own? override() derives a child of a theme you built. To build on top of a published theme instead — reusing its variables without redefining them — declare the borrowed tokens as external: colors: { brand: { external: "colors.brand" } } with a top-level extends: { prefix } emits var(--dt-colors-brand), referenceable everywhere and never redefined. Literal form { external: "--any-var" } points at any parent (Material / Tailwind / hand-rolled). See the authoring guide.

2 · Same classes, different values

The composition is structural, so every brand resolves to the same class list. Only each brand's stylesheet redefines the variables — which means your components are brand-agnostic:

app.ts
acme.getClass("components","buttons","primary") === base.getClass(...);  // → true
// every brand: "dt-colors-solid-brand dt-components-buttons-primary"

// only the emitted variable differs per brand:
//   base.css   → --dt-colors-brand: rgb(76, 110, 245);
//   acme.css   → --dt-colors-brand: rgb(232, 89, 12);
//   globex.css → --dt-colors-brand: rgb(47, 158, 68);

3 · Serve the right brand

Because markup is identical, delivering a brand is just choosing its stylesheet:

WhereHow
Run-timeHold the active brand in state and inject brand.css — the React swap pattern.
Per-request (SSR)Pick the brand from the request (subdomain / tenant) and stream that brand's <style>.
Build-timeEmit each brand to its own file (acme.css, globex.css) and serve per tenant — see Build-time.

Why override(), not N full themes

override() is a delta merge, not a re-run: only the changed subsystem slices re-normalize, everything untouched keeps its reference (structural sharing), and the base is left byte-identical. So spinning up dozens of brands off one base is cheap — each is a thin delta, not a full theme rebuild.

This site's preset switcher is white-labeling: four themes with identical class names, and “Halcyon Noir” is a theme.override() child. See Run-time for the override() mechanics and React for wiring the swap.
Technical documentation for @theme-registry/refract · all output is compiled client-side by the real library (CSS adapter). The chrome is theme-aware; the render panes carry each preset's own world. · MIT licensed.