Theming
Override CSS custom properties to make the design system yours. Every token resolves automatically in light and dark mode.
How theming works
Changing the accent color
The most common customization. Define both light and dark values:
:root {
--accent: oklch(0.65 0.22 150);
}
.dark {
--accent: oklch(0.72 0.19 150);
}This changes links, active states, and primary buttons across every component. Colors use the OKLCH color space for perceptual uniformity, so equal lightness steps look equally bright.
What can I theme?
Every token category can be overridden. See the design system pages for the full catalog.
| Category | Tokens |
|---|---|
| Colors | Accent, destructive, warning, success, surfaces, text hierarchy |
| Typography | 11 sizes from 2xs to jumbo, font families, weights, tracking |
| Spacing | Base unit, container width, max-width scale, breakpoints |
| Shadows | Four-level shadow scale from xs to lg |
| Radii | Component tier system: form controls, buttons, cards, modals, indicators |
Overriding multiple tokens
Override as many or as few tokens as you need:
:root {
/* Colors */
--accent: oklch(0.65 0.22 150);
--background: oklch(0.98 0.005 90);
--card: oklch(1 0 0);
/* Radius (override the scale step you want to reshape) */
--radius-lg: 0.75rem;
}
.dark {
--accent: oklch(0.72 0.19 150);
--background: oklch(0.15 0.005 90);
--card: oklch(0.18 0.005 90);
}Each component tier reads a step of the --radius-* scale: inputs use --radius-md, buttons and cards use --radius-lg, drawers and sheets use --radius-xl. Override the step you want to reshape.
Typography
Geist ships with the kit as the system face; font families still resolve through a chain, so one override swaps it.
Default: Geist Sans and Geist Mono
The kit installs the geistpackage (Vercel’s own build, fonts only, no runtime code), because the system’s typography is tuned to Geist’s letterforms: the token file enables its stylistic sets on body, and only the publisher’s package carries them, the same stripping problem the callout below explains. A font cannot wire itself, so this is the one manual step: two lines in your root layout, checked by verify-ui so an unwired chain warns instead of silently rendering system-ui. This site takes exactly this rung: no override, the same face a fresh install gives you.
// app/layout.tsx
import { GeistSans } from "geist/font/sans"
import { GeistMono } from "geist/font/mono"
<html className={`${GeistSans.variable} ${GeistMono.variable}`}>A face you host yourself
Host the file yourself when the face is commercial and cannot ship with a registry, or when a hosted copy is not good enough. This site does not take this rung, and the reason is worth knowing: a self-hosted face cannot ship with a registry at all, because a registry item carries text and a font file is binary. Load a local file with next/font/local:
// app/layout.tsx
import localFont from "next/font/local"
const brandFace = localFont({
src: "./fonts/YourBrandFace.woff2",
variable: "--font-brand",
weight: "100 900",
display: "swap",
}):root {
--font-sans-custom: var(--font-brand);
}A face from a package
Most free faces install from npm. Import the package for its side-effect, then name the family directly. Note the literal family name rather than a var(): a package ships plain @font-facerules, so there is no generated variable to point at, and the name has to match the package’s own exactly or the chain falls through with no error anywhere.
// app/layout.tsx
import "@fontsource-variable/your-face":root {
--font-sans-custom: "Your Face Variable", ui-sans-serif, system-ui, sans-serif;
}Any other font
The hook is not specific to either route. Load any font with next/font, then point --font-sans-custom (or --font-mono-custom for code) at its variable to override the default without touching component code.