Overview

shadcn/ui theming lives entirely in CSS variables declared on :root and .dark in globals.css. Every component references those variables; none hardcode a color. Change the variables, and every component updates. This page covers how to work within that system: adjusting the palette, adding brand tokens, enabling dark mode, and avoiding the escape hatches that break the model.

Edit tokens in globals.css, not in component files

The --background, --foreground, --primary, --ring, and other semantic tokens are declared once in globals.css. Components use utilities like bg-background and text-primary that map to those tokens. To retheme, change the tokens. Do not open a component file to patch a color class.

:root {
  --background: oklch(1 0 0);
  --foreground: oklch(0.145 0 0);
  --primary: oklch(0.55 0.18 264);
  --primary-foreground: oklch(0.985 0 0);
  --secondary: oklch(0.97 0 0);
  --secondary-foreground: oklch(0.205 0 0);
  --muted: oklch(0.97 0 0);
  --muted-foreground: oklch(0.556 0 0);
  --accent: oklch(0.97 0 0);
  --accent-foreground: oklch(0.205 0 0);
  --destructive: oklch(0.577 0.245 27.3);
  --destructive-foreground: oklch(0.985 0 0);
  --border: oklch(0.922 0 0);
  --ring: oklch(0.55 0.18 264);
  --radius: 0.625rem;
}

The current CLI default writes full oklch() color functions and maps them into Tailwind with @theme inline (no tailwind.config file). A project scaffolded before the Tailwind v4 switch may still carry bare HSL channels (--primary: 221.2 83.2% 53.3%) composed as hsl(var(--primary) / <alpha-value>) in tailwind.config.ts. Both formats work; pick one per project and do not mix them. See css-custom-properties for the general pattern and shadcn-installation for the CLI defaults.

Define dark mode with a .dark class selector, not prefers-color-scheme

shadcn uses the class strategy: add .dark to the <html> element to activate dark mode. This gives users runtime control without CSS specificity fights.

.dark {
  --background: oklch(0.145 0 0);
  --foreground: oklch(0.985 0 0);
  --primary: oklch(0.7 0.18 264);
  --primary-foreground: oklch(0.205 0 0);
  --border: oklch(1 0 0 / 10%);
  --ring: oklch(0.7 0.18 264);
}

Toggle dark mode in React with a state value on the root element:

document.documentElement.classList.toggle("dark", isDark);

Pair this with localStorage persistence and a server-rendered cookie so the class is set before paint. A flash of wrong theme is a layout shift that hurts CLS. See tailwind-dark-mode for the Tailwind class-strategy configuration.

Map tokens to a custom brand palette with oklch

Replace the default palette with your brand colors as oklch values. Most design tools export hex; convert to oklch before pasting (most color pickers and browser DevTools do this conversion natively now).

:root {
  /* Brand: Indigo 600 ≈ oklch(0.51 0.23 276) */
  --primary: oklch(0.51 0.23 276);
  --primary-foreground: oklch(1 0 0);
 
  /* Brand: Slate 900 for text */
  --foreground: oklch(0.21 0.03 265);
}

oklch’s lightness channel is perceptually uniform: moving from oklch(0.55 ...) to oklch(0.7 ...) looks like the same visual step regardless of hue, which makes generating a dark-mode counterpart mechanical instead of a fresh pick. On a legacy HSL-token project, the same edit uses raw channel triples (--primary: 243 75% 59%) instead. Use a contrast checker to verify that --primary-foreground is legible on --primary. The WCAG AA minimum is 4.5:1 for normal text. A brand color that fails contrast should be lightened or darkened; do not adjust the token to cheat the check.

Add custom tokens for components that fall outside the semantic set

The shadcn token set covers interactive semantics. When a page layout, a chart, or a data-dense component needs colors outside that set, add new variables rather than reusing semantics with wrong meaning.

:root {
  --chart-1: oklch(0.65 0.22 41);
  --chart-2: oklch(0.6 0.12 185);
  --chart-3: oklch(0.4 0.07 227);
  --sidebar-background: oklch(0.21 0 0);
  --sidebar-foreground: oklch(0.97 0 0);
}

Name tokens by semantic role, not by visual value. --sidebar-background is good; --gray-950 is an internal detail. With Tailwind v4, register the token in the @theme inline block so it becomes a utility. See tailwind for the v4 CSS-first config.

Check browser support before committing fully to oklch

oklch reached Baseline across evergreen browsers in 2023 and is safe to ship in 2026 without a fallback. When mixing oklch and legacy HSL tokens during a migration, keep the @theme inline (or tailwind.config.ts extend.colors) mapping consistent with whichever format each token actually uses; do not silently convert one without updating the other.

Avoid overriding colors inside component files

Patching a color directly in button.tsx or card.tsx works once. It fails when the theme changes, when dark mode toggles, or when another developer edits the token. The rule is: if a style is a theme decision, it belongs in globals.css. If it is a one-off layout adjustment, use a Tailwind utility class passed via className on the callsite rather than editing the shared file.

The exception is when a component genuinely needs a new variant that no token covers. In that case, add the variant to the component file using cva (class-variance-authority) and document the decision. See shadcn-composition for the variant-extension pattern.