Theming
Brand the components by giving the colour tokens your own values; the components follow.
How it works
Components never name a colour. They use semantic tokens, such as bg-primary or text-muted-foreground, and theme.css gives each token a neutral value for the light theme (:root) and the dark theme (.dark).
To brand them, write a brand.css that gives the tokens your values, and import it after the theme:
@import "tailwindcss";
@import "@booleanpress/ui/theme.css";
@import "./brand.css";brand.css holds values for token names in :root and .dark, nothing else. A token you leave out keeps its default. The names are shadcn/ui's, so a shadcn/ui theme works unchanged.
Build a palette
Pick a primary colour and a radius. The components below change as you pick, and the contrast check runs on every change. Copy the result into brand.css.
/* brand.css — after @import "@booleanpress/ui/theme.css"; */
:root {
--primary: oklch(0.55 0.2 262);
--primary-foreground: oklch(0.985 0 0);
--ring: var(--primary);
--sidebar-primary: var(--primary);
--sidebar-primary-foreground: var(--primary-foreground);
--radius: 0.625rem;
}
.dark {
--primary: oklch(0.72 0.16 262);
--primary-foreground: oklch(0.145 0 0);
}bui-contrast: every text/surface pair ≥ 4.5:1 and every control edge ≥ 3:1 (light and dark), 50 pairs.
The tokens
| Token | What it paints |
|---|---|
--background, --foreground |
The page and its text. |
--card, --card-foreground |
Cards and other raised blocks. |
--popover, --popover-foreground |
Floating surfaces: menus, popovers, selects, dialogs, sheets and toasts. |
--primary, --primary-foreground |
The main action, checked controls, the selected item, progress. |
--secondary, --secondary-foreground |
Secondary buttons and badges. |
--muted, --muted-foreground |
Quiet surfaces (the tab list, skeletons) and secondary text. |
--accent, --accent-foreground |
The highlighted item in a menu or list, hovered ghost buttons. |
--destructive, --destructive-foreground |
Destructive actions, errors and invalid fields. |
--success, --warning, --info and their -foreground |
The status variants of Alert, Badge and the toasts. |
--success-strong, --destructive-strong, --info-strong |
Status colours dark enough for text and for filled badges. |
--border, --input, --ring |
Lines, field fills in the dark theme, and the focus ring. |
--control |
The edge of a checkbox, radio, switch track, text field or select: at least 3:1 against every surface it sits on. |
--chart-1 … --chart-5 |
The series colours of charts, through each chart's config. |
--sidebar and its -foreground, -primary, -accent, -border, -ring |
The sidebar, which may differ from the page. |
--radius |
Corner rounding; components use it and steps derived from it. |
Each component page lists the tokens it reads under Theming.
Check contrast
The package ships a command that checks your palette. Add it to your lint script:
{
"scripts": {
"lint": "eslint src && bui-contrast src/brand.css"
}
}bui-contrast reads brand.css on top of the defaults and measures every text colour the components draw against each surface it is drawn on, and every form control's edge against the surfaces it sits on, in the light and the dark theme. It exits with an error when a pair falls below WCAG 2.2 AA (4.5:1 for normal text, 3:1 for a control's edge, so an unchecked checkbox stays visible) and names the pair:
FAIL light text-primary-foreground on bg-primary 3.12:1
[contrast] src/brand.css — 1 of 50 pairs below their minimum (text 4.5:1, control edges 3:1)Changing one component
Every part accepts className, merged after its own classes, so a utility you pass wins over the component's: <Button className="rounded-full">. Prefer a token change when the same change should apply everywhere, and className for a single place.