Theming
Re-brand the whole system by overriding one layer of tokens.
The contract
Components reference semantic roles only — never a primitive ramp and never a hex value. That single rule is what makes theming a CSS edit rather than a fork. If you ever have to open a component file to change a colour, treat it as a bug in the system.
Change the brand colour
Override the roles in your own stylesheet, after the import. Both themes need doing — a colour that reads on white rarely reads on near-black.
:root {
--primary: oklch(0.62 0.19 259); /* your brand */
--primary-foreground: oklch(0.98 0 0);
--ring: var(--primary);
}
.dark {
--primary: oklch(0.70 0.16 259); /* lifted for dark surfaces */
--primary-foreground: oklch(0.15 0 0);
--ring: var(--primary);
}Change the corner language
Every radius is calc() off one base. Set it to 0rem for a square system, or 1.5rem for a soft one.
:root {
--radius: 0.25rem; /* sharp */
}Change the typeface
// app/layout.tsx
import { Inter } from "next/font/google";
const inter = Inter({ variable: "--font-outfit", subsets: ["latin"] });The variable name is what the theme reads, so pointing it at a different font is enough. For Arabic, Hebrew, or Persian interfaces, load a face with real coverage of those scripts — Latin fallbacks render Arabic without proper joining.
Dark mode
Driven by a .dark class on <html>, managed by next-themes. The @custom-variant declaration in the token layer is what makes dark: utilities resolve against the class rather than the OS setting.
@custom-variant dark (&:is(.dark *));
What not to do
- Don't reference primitive ramps in application code —
bg-brand-500hardcodes a brand into a component and defeats theming. - Don't add one-off colours. If a value is worth using twice, it is worth a semantic role.
- Don't override a role in only one theme. A half-themed system fails the moment a user toggles.
Verify both themes and both directions