Overview

shadcn/ui theming is a set of semantic CSS variables in globals.css: :root holds the light values, .dark overrides them, and @theme inline maps them to Tailwind utilities. Components use bg-primary and text-foreground, never raw colors, so editing variables restyles everything. Setup is in shadcn.

Edit tokens in globals.css, not in components

Tokens come in surface and foreground pairs. The base name is the surface; the -foreground name is the text and icon color on it, so bg-primary text-primary-foreground.

The current token set: background, foreground, card, popover, primary, secondary, muted, accent (each with a -foreground pair), plus destructive, border, input, ring, chart-1 to chart-5, the sidebar-* family, and radius. There is no destructive-foreground, and the sidebar surface is --sidebar.

@import "tailwindcss";
@import "shadcn/tailwind.css";
 
@custom-variant dark (&:is(.dark *));
 
:root {
  --radius: 0.625rem;
  --background: oklch(1 0 0);
  --foreground: oklch(0.145 0 0);
  --primary: oklch(0.205 0 0);
  --primary-foreground: oklch(0.985 0 0);
}
.dark {
  --background: oklch(0.145 0 0);
  --foreground: oklch(0.985 0 0);
  --primary: oklch(0.922 0 0);
  --primary-foreground: oklch(0.205 0 0);
}
 
@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --color-primary: var(--primary);
  --color-primary-foreground: var(--primary-foreground);
}

:root and .dark sit outside @layer base, and @theme inline holds var() references rather than color functions. A project scaffolded before the Tailwind v4 switch may still use hsl(var(--x)) wrappers; migrate the whole token set instead of mixing formats.

Add tokens in three places

Define the variable in :root and in .dark, then expose it in @theme inline.

:root { --warning: oklch(0.84 0.16 84); --warning-foreground: oklch(0.28 0.07 46); }
.dark { --warning: oklch(0.41 0.11 46); --warning-foreground: oklch(0.99 0.02 95); }
@theme inline {
  --color-warning: var(--warning);
  --color-warning-foreground: var(--warning-foreground);
}

Name tokens by role (--warning), not by hue.

Derive radii from one variable

--radius is the base; @theme inline derives --radius-sm to --radius-4xl with calc(var(--radius) * 0.6) and similar multipliers. Change --radius to restyle every corner.

Toggle dark mode with a .dark class

The scaffold’s dark variant matches a .dark ancestor. Add and remove .dark on <html> from a theme provider, and set it before paint to avoid a flash; the docs have a guide per framework. For the Tailwind side, see tailwind-dark-mode.

Choose the brand palette in oklch and verify contrast

Convert brand colors to oklch (DevTools color pickers do this). Check that each -foreground token reaches 4.5:1 on its surface; if the brand color fails, adjust the token, not the check. oklch() is Baseline widely available (since November 2025), so no fallback is needed. The scaffold offers base colors neutral, stone, zinc, mauve, olive, mist, and taupe.

Avoid overriding colors in component files

A color patched inside button.tsx breaks when the theme or mode changes. If a style is a theme decision, change the token. For a one-off, pass a className at the call site. Add a new variant to the component only when no token covers it; see shadcn-composition.