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.

@layer base {
  :root {
    --background: 0 0% 100%;
    --foreground: 222.2 84.7% 4.1%;
    --primary: 221.2 83.2% 53.3%;
    --primary-foreground: 210 40% 98%;
    --secondary: 210 40% 96.1%;
    --secondary-foreground: 222.2 47.4% 11.2%;
    --muted: 210 40% 96.1%;
    --muted-foreground: 215.4 16.3% 46.9%;
    --accent: 210 40% 96.1%;
    --accent-foreground: 222.2 47.4% 11.2%;
    --destructive: 0 84.2% 60.2%;
    --destructive-foreground: 210 40% 98%;
    --border: 214.3 31.8% 91.4%;
    --ring: 221.2 83.2% 53.3%;
    --radius: 0.5rem;
  }
}

The values are HSL channels, not full color functions. Tailwind composes them as hsl(var(--primary) / <alpha-value>), which enables opacity utilities. Keep this format when changing values. See css-custom-properties for the general pattern.

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: 222.2 84% 4.9%;
  --foreground: 210 40% 98%;
  --primary: 217.2 91.2% 59.8%;
  --primary-foreground: 222.2 47.4% 11.2%;
  --border: 217.2 32.6% 17.5%;
  --ring: 224.3 76.3% 48%;
}

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 by changing hsl channel values

Replace the default palette with your brand colors by computing the HSL channels. Most design tools export hex; convert to HSL before pasting.

:root {
  /* Brand: Indigo 600 = hsl(243 75% 59%) */
  --primary: 243 75% 59%;
  --primary-foreground: 0 0% 100%;
 
  /* Brand: Slate 900 for text */
  --foreground: 222 47% 11%;
}

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: 12 76% 61%;
  --chart-2: 173 58% 39%;
  --chart-3: 197 37% 24%;
  --sidebar-background: 240 5.9% 10%;
  --sidebar-foreground: 240 4.8% 95.9%;
}

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

Use oklch for perceptually uniform palettes

The default shadcn tokens use hsl. oklch produces more perceptually uniform palette steps, especially across hue shifts. When building a custom design system on top of shadcn, consider converting to oklch:

:root {
  --primary: oklch(0.55 0.18 264);
  --primary-foreground: oklch(0.98 0 0);
  --background: oklch(1 0 0);
}

When switching to oklch, update Tailwind’s @theme mapping to match. Mix-in color-mix() for tint and shade steps if needed. Check browser support: oklch is baseline Interop 2025 and is safe for production.

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.