Getting started

Theming

Monochrome by default, one brand knob, light and dark from the same roles.

Blank ships black and white. Ink on neutral greys, with color reserved for what it means: status (success, warning, info, danger), data, and your brand. Every color a component uses is a role (background, primary, brand, muted-foreground) defined as a CSS variable, once for light (:root) and once for dark (.dark). A new look is an edit to those variables, never to a component.

The fastest way to make one is the theme configurator: pick a brand color, tint the greys, set the radius and the font, and copy only the variables that changed.

Light and dark

Dark mode is a dark class on <html>. In Next.js, use next-themes:

<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
  {children}
</ThemeProvider>

Tailwind's dark: variant follows the same class.

The brand knob

brand is the one role meant to be turned. Focus rings, checked boxes, radios and switches, selected days, the active tab line, progress and slider fills all use it. Out of the box it is the same ink as primary, so nothing on the page is colored until you say so.

Set it per theme. Dark mode wants a lighter step of the same color, and the foreground is whichever of white or ink reads better on the fill:

:root {
  --brand: #2563eb;
  --brand-hover: color-mix(in oklab, var(--brand) 85%, var(--background));
  --brand-subtle: color-mix(in oklab, var(--brand) 10%, var(--background));
  --brand-foreground: var(--white);
}

.dark {
  --brand: #5182ef;
  --brand-hover: color-mix(in oklab, var(--brand) 85%, var(--background));
  --brand-subtle: color-mix(in oklab, var(--brand) 16%, var(--background));
  --brand-foreground: var(--neutral-950);
}

ring, focus-ring, item-selected and item-selected-border are mixed from --brand, so they follow without being listed.

primary stays separate on purpose. It is the action color, the filled button, and it stays ink, so a screen full of buttons does not glow in the brand color.

Tint the neutrals

Every surface, border and line of text reads from one grey ramp, --neutral-50 to --neutral-950, shared by both themes. Override the ramp in :root and light and dark warm up (or cool down) together:

:root {
  --neutral-100: #f6f5f4;
  --neutral-300: #e3e0dd;
  --neutral-600: #6a635c;
  --neutral-900: #191715;
  /* …the other steps from the configurator */
}

Keep the tint faint. A few points of red or blue in the mid greys is enough; white and near-black stay almost pure.

Radius

Every radius derives from --radius (default 0.625rem, 10px), so the system rounds together: cards sit at --radius + 6px, dialogs at --radius + 14px. Pill controls (buttons, inputs, tabs, badges) stay round at any value. The full scale is on Tokens.

:root {
  --radius: 0.875rem;
}

Fonts

The token sheet reads two variables: --font-geist-sans for font-sans and --font-geist-mono for font-mono. Load Geist with next/font under those names, as in Installation. To use another family, load it under the same variable:

const sans = Inter({ subsets: ["latin"], variable: "--font-geist-sans" })

Override any role

The brand is the knob; everything else can still move. Re-point a role in both blocks so light and dark stay a pair, and prefer that over classes on components: a button that reads bg-primary follows a new --primary, one you gave a palette class does not.

Inverted surfaces

A dark menu on a light page is a surface, not a second palette. Put data-inverted on a popover-like element and it re-points the popover roles at their inverted counterparts:

<SelectContent inverted>…</SelectContent>