UI.MD

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.

CategoryTokens
ColorsAccent, destructive, warning, success, surfaces, text hierarchy
Typography11 sizes from 2xs to jumbo, font families, weights, tracking
SpacingBase unit, container width, max-width scale, breakpoints
ShadowsFour-level shadow scale from xs to lg
RadiiComponent 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.

Next steps