Theming
Theming is a stylesheet, not an API. Override the tokens you want to change and every component follows — no provider to mount, no context, no runtime cost, and it works unchanged inside Server Components.
How it works
Components reference semantic tokens like --abba-primary rather than literal colours. Redefining that property anywhere in the cascade changes every component below it. Because custom properties inherit, the scope of a theme is just the element you declare it on.
Rebranding
Start with the semantic layer. You rarely need to touch the Cedar and Ember palettes themselves — those exist so the semantic tokens have somewhere sensible to point by default.
/* app/theme.css — imported after @abbainitiative/ui/styles.css */
:root {
/* Repoint the semantic layer at your own palette. */
--abba-primary: #4338ca;
--abba-primary-hover: #3730a3;
--abba-primary-active: #312e81;
--abba-primary-foreground: #ffffff;
--abba-primary-subtle: #eef2ff;
--abba-primary-subtle-foreground: #312e81;
--abba-accent: #be185d;
--abba-accent-hover: #9d174d;
--abba-accent-foreground: #ffffff;
}Order matters
@abbainitiative/ui/styles.css. Both declare at :root, so identical specificity means source order decides the winner.Shape
Radii carry as much brand identity as colour does. Flattening them changes the system's character more than most palette swaps.
/* A squarer, tighter system. */
:root {
--abba-radius-sm: 2px;
--abba-radius-md: 3px;
--abba-radius-lg: 4px;
--abba-radius-xl: 6px;
}Typography
The library ships no webfont, so there is nothing to override away — point --abba-font-sans at whatever you already load.
/* next/font, or any font you already load. */
import { Inter } from "next/font/google";
const inter = Inter({ subsets: ["latin"], variable: "--app-font" });
/* Then in your stylesheet: */
:root {
--abba-font-sans: var(--app-font), system-ui, sans-serif;
}Scoped themes
A theme does not have to be global. Declare tokens on any element and only its subtree changes — useful for a marketing section, a tenant-specific area, or a preview pane showing another brand.
/* Themes do not have to be global. Any element can open a new
scope, and everything inside it inherits. */
.marketingSection {
--abba-primary: var(--abba-ember-500);
--abba-primary-hover: var(--abba-ember-600);
--abba-radius-md: var(--abba-radius-full);
}When tokens are not enough
Every component forwards className and merges it after its own classes, so a CSS module class wins on source order without needing !important. Reach for this when you need a one-off; if you find yourself doing it repeatedly for the same reason, that is usually a missing token rather than a missing override.
import { Button } from "@abbainitiative/ui";
import styles from "./checkout.module.css";
/* Every component forwards className and merges it after its own,
so a module class wins without !important. */
<Button className={styles.checkoutButton}>Pay now</Button>Check your contrast
The default palette was tuned to clear WCAG AA at every pairing the components actually use. An override discards that work, so verify the result rather than trusting how it looks.
/* Verify overrides, do not assume them. A palette that looks
right at a glance frequently fails AA:
text on --abba-primary → needs 4.5:1
large text (18.66px+ bold, 24px+) → needs 3:1
focus ring against its neighbour → needs 3:1
borders that convey state → needs 3:1 */Next: dark mode, which is the same mechanism applied through a selector.