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.