Design system

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

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.