Author a brand once, compile it everywhere. brandc is a tiny brand compiler: describe a
Brand as structured data (design tokens) and generate a CSS custom-property contract, a Tailwind
v4 preset, and a prefab-wire theme from it. One variable vocabulary across any stack — SSR string
injection, bundler/CDN, plain HTML, React — so a brand is authored once and works everywhere.
Ships an example brand you can use as-is or override.
-
The contract — the names of the variables (
--primary,--card,--radius,--success,--font-sans, …). Stable across kits and stacks. SeeCONTRACT/TokenName. -
A brand — the values. This package ships two brands on that one contract, deliberately different so it is clear the vocabulary is brand-agnostic:
maxhealth— flat + sharp (--radius: 0, no shadows), neutral intents + green accent (CC BY 4.0).dashboard— rounded + soft-shadowed, blue primary, slate neutrals, dark app sidebar (MIT).
Rebrand = a new
Brandwith the same names, different values.
The single source of truth is structured data in src/tokens.ts (each colour as a
{ light, dark } pair). Every delivery format is generated from it by
src/compile.ts, so they cannot drift:
| Output | For | Import |
|---|---|---|
THEME_CSS (string) |
SSR string-injection | import { THEME_CSS } from "brandc" |
theme.css (file) |
bundler / CDN / plain HTML | import "brandc/theme.css" |
tailwind.css (file) / TAILWIND_CSS |
Tailwind v4 @theme inline preset |
import "brandc/tailwind.css" |
toPrefabTheme(brand) |
prefab wire theme JSON |
import { toPrefabTheme } from "brandc" |
base.css (file) / BASE_CSS |
opt-in element ergonomics (see below) | import "brandc/base.css" |
The contract is pure token vocabulary. base.css is a separate, optional stylesheet for one
cross-cutting framework quirk every Tailwind v4 consumer hits: Tailwind v4 dropped the default
cursor: pointer on <button> (it follows the native default now). Import it if you want it:
@import "brandc/base.css"; /* restores pointer cursor on buttons, links, [role=button], … */Plain low-specificity CSS (:where(...)) on element selectors — no tokens, easily overridden.
SSR/string consumers that set cursors inline, and non-CSS consumers (React Native), just don't
import it. It is never bundled into theme.css / tailwind.css.
light-dark()— each colour is one declaration (--bg: light-dark(<light>, <dark>)), not a duplicated dark block.:root { color-scheme: light dark }honoursprefers-color-schemeautomatically;.dark/[data-theme="dark"](and.light/[data-theme="light"]) flipcolor-schemefor a manual override. Both class anddata-themeconventions are supported.@property— colour tokens are registered as<color>(type-safety + animatable).- oklch everywhere; derived surfaces via
color-mix()in the consuming component CSS (no-bgtoken sprawl).
Raw values live in :root/dark (theme.css); tailwind.css is a non-inline-value @theme inline
layer that maps them to Tailwind's namespaces by reference — so utilities like bg-primary exist
and runtime dark switching still works (mapping with var(), never baking values at build time):
@import "tailwindcss";
@import "brandc/theme.css";
@import "brandc/tailwind.css";import { toCss, type Brand } from "brandc";
import { maxhealth } from "brandc";
const ocean: Brand = {
name: "ocean",
colors: { ...maxhealth.colors, primary: { light: "oklch(0.58 0.06 195)", dark: "oklch(0.7 0.06 195)" } },
scalars: { ...maxhealth.scalars, radius: "0.625rem", "radius-lg": "0.875rem", shadow: "0 1px 2px rgb(23 32 31 / 0.05)" },
};
const css = toCss(ocean); // teal, rounded, soft-shadow — same contract, works on every stackOr start from a shipped brand: import { dashboard, DASHBOARD_THEME_CSS } from "brandc" for a
rounded, blue, dark-sidebar dashboard look, then override a few tokens.
When a Tailwind v4 app defines its own brand and only wants to rebrand colours (keeping
Tailwind's own rounded-* / shadow-* / font scale), pass scalars: {}. The contract's
--radius* / --shadow* / --font-* names are the same keys Tailwind v4 uses in @theme, so
emitting them would override Tailwind's (e.g. Max Health's --radius: 0 flattens every rounded-*).
Empty scalars keeps it colours-only:
const brand: Brand = { name: "secretspots", colors: { /* … */ }, scalars: {} };
// inject toCss(brand) + toTailwindCss(brand); bg-primary/text-foreground map to the brand,
// rounded-lg/shadow-md stay Tailwind's own.Also add a .light class alongside your app's .dark toggle so color-scheme / light-dark()
resolve (otherwise OS dark can leak into light mode).
- gaestehaus-schaub.at — an SSR shell and a precompiled-Tailwind public site from one brand.
src/lib/brand.tsdefines the brand (teal);toCss(brand)is injected in each<head>, andapp.cssmaps Tailwind utilities to it by reference (--color-brand: var(--brand)) — no build step, no hardcoded palette. - secretspots.guide — its own brand on Tailwind v4 with
scalars: {}(the pattern above). - hono-ui — the admin theme is compiled from a
BrandviatoCss, so the kit's palette can never drift from the contract.
npm install
npm run check # tsc --noEmit && build && node --test (TypeScript 7)
npm run build # tsc → dist/, then regenerate theme.css + tailwind.css from the sourcesrc/tokens.ts is the single source of truth; the .css files are generated by
scripts/build-css.mjs and kept in sync by a test. Edit the TypeScript, never the .css.
- Code — the compiler, the token contract, scripts: MIT.
- Bundled
maxhealthexample brand — CC BY 4.0: if you ship that brand, credit Max Health Inc. and link https://maxhealth.tech. Author your ownBrandand only the MIT terms apply. SeeLICENSE/NOTICE.