Skip to the content
BooleanPress UI

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.

Delivery
The components below follow the tokens you pick.
ActiveDraft
Two mailers send this site's email.
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.