Skip to content

max-network/brandc

Repository files navigation

brandc

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.

Two axes

  • The contract — the names of the variables (--primary, --card, --radius, --success, --font-sans, …). Stable across kits and stacks. See CONTRACT / 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 Brand with the same names, different values.

Authored once, generated many

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"

Opt-in base ergonomics (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.

Modern CSS, on purpose

  • light-dark() — each colour is one declaration (--bg: light-dark(<light>, <dark>)), not a duplicated dark block. :root { color-scheme: light dark } honours prefers-color-scheme automatically; .dark / [data-theme="dark"] (and .light / [data-theme="light"]) flip color-scheme for a manual override. Both class and data-theme conventions 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 -bg token sprawl).

Tailwind v4 (two-stage, runtime-switchable)

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";

Rebrand (override, once)

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 stack

Or 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.

Tailwind v4: brand your own app (the scalars gotcha)

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).

Consumers in the wild

  • gaestehaus-schaub.at — an SSR shell and a precompiled-Tailwind public site from one brand. src/lib/brand.ts defines the brand (teal); toCss(brand) is injected in each <head>, and app.css maps 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 Brand via toCss, so the kit's palette can never drift from the contract.

Development

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 source

src/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.

License

  • Code — the compiler, the token contract, scripts: MIT.
  • Bundled maxhealth example brandCC BY 4.0: if you ship that brand, credit Max Health Inc. and link https://maxhealth.tech. Author your own Brand and only the MIT terms apply. See LICENSE / NOTICE.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages