Use when wiring shadcn ui colors, dark mode, the design-token palette, or migrating a project between Tailwind v3 and Tailwind v4, or when pasting theme output from https://ui.shadcn.com/themes into a project. Prevents the single largest source of AI-generated style bugs in shadcn projects : the HSL-space-separated (Tailwind v3) versus oklch (Tailwind v4) format split, the hsl(var(--background)) wrapper convention versus the bare var(--background) convention, the bg-background utility semantics difference, and dark-mode flash-of-unstyled-content from a missing suppressHydrationWarning on the html element. Covers the exhaustive CSS-custom-property token catalog (core, sidebar, chart, radius scale), the @theme inline directive (v4) versus the tailwind.config.js theme.extend.colors block (v3), the next-themes ThemeProvider wiring on Next.js, the custom localStorage ThemeProvider on Vite, the useTheme hook, the mode-toggle component, and the copy-from-themes-page workflow. Keywords: shadcn theming, design tokens,
Use when wiring shadcn ui colors, dark mode, the design-token palette, or migrating a project between Tailwind v3 and Tailwind v4, or when pasting theme output from https://ui.shadcn.com/themes into a project. Prevents the single largest source of AI-generated style bugs in shadcn projects : the HSL-space-separated (Tailwind v3) versus oklch (Tailwind v4) format split, the hsl(var(--background)) wrapper convention versus the bare var(--background) convention, the bg-background utility semantics difference, and dark-mode flash-of-unstyled-content from a missing suppressHydrationWarning on the html element. Covers the exhaustive CSS-custom-property token catalog (core, sidebar, chart, radius scale), the @theme inline directive (v4) versus the tailwind.config.js theme.extend.colors block (v3), the next-themes ThemeProvider wiring on Next.js, the custom localStorage ThemeProvider on Vite, the useTheme hook, the mode-toggle component, and the copy-from-themes-page workflow. Keywords: shadcn theming, design tokens, CSS custom properties, CSS variables, --background, --foreground, --primary, --secondary, --muted, --accent, --destructive, --border, --input, --ring, --radius, HSL space separated, oklch, Tailwind v3, Tailwind v4, @theme inline, tailwind.config.js colors, hsl var background, bg-background, dark mode, next-themes, ThemeProvider, useTheme, suppressHydrationWarning, flash of unstyled content, FOUC, theme builder, ui.shadcn.com themes, copy theme CSS, light dark system, default new-york sera luma, why are my colors wrong, why is my theme not applying, bg-background not working, hsl wrapper, oklch wrapper, my colors look gray, how do I add dark mode, how do I change the primary color, custom palette, theme override, tw-animate-css, @custom-variant dark, semantic colors, color tokens, design system colors.
license
MIT
compatibility
Designed for Claude Code. Requires shadcn ui evergreen-2026.
metadata
{"author":"OpenAEC-Foundation","version":"1.0"}
shadcn ui : Theming
Deterministic mental model for shadcn theming. The HSL versus oklch split, the wrapper conventions, and the dark-mode wiring are the single largest source of AI-generated style bugs in shadcn projects. ALWAYS identify the Tailwind generation FIRST, then pick the matching wiring.
Quick Reference
One question, one answer
ALWAYS ask first : "Is the project Tailwind v3.4 or Tailwind v4?" Every theming decision branches from that answer. NEVER mix v3 and v4 conventions in the same globals.css.
v3 vs v4 at a glance
Concern
Tailwind v3.4
Tailwind v4
Color format in CSS vars
HSL space-separated : 0 0% 100%
oklch : oklch(1 0 0)
Wiring location
tailwind.config.js extends colors
globals.css@theme inline block
Variable usage in utilities
bg-[hsl(var(--background))] (in arbitrary) or bg-background (after wiring)
ALWAYS verify the project's Tailwind generation by checking package.json for "tailwindcss": "^4..." versus "tailwindcss": "^3...". NEVER infer from filenames.
The bg-background semantic trap
ALWAYS understand : bg-background is NOT a built-in Tailwind utility. It exists ONLY when one of the following is true :
(v3) tailwind.config.js has theme.extend.colors.background = "hsl(var(--background))"
(v4) globals.css has @theme inline { --color-background: var(--background); }
NEVER write bg-background in a project that has neither wiring : the class silently produces no style and AI assistants will repeatedly suggest it. Diagnose missing utility by inspecting the generated CSS, NEVER by guessing.
Five invariants
ALWAYS write CSS variable values in the format dictated by the project's Tailwind generation. HSL space-separated for v3 (no commas, no hsl() wrapper inside the variable). oklch for v4.
ALWAYS define .dark as a class selector overriding the SAME variables. NEVER use @media (prefers-color-scheme: dark) directly in shadcn projects; the toggle uses a class.
ALWAYS map raw variables into Tailwind's color namespace : via tailwind.config.js in v3, via @theme inline in v4. The variables alone do nothing for Tailwind utilities until they are mapped.
ALWAYS put suppressHydrationWarning on the <html> element when using next-themes in Next.js, and add the attribute="class" prop on the ThemeProvider. Omitting either produces flash-of-unstyled-content and hydration warnings.
ALWAYS use cn() (from @/lib/utils) for any conditional class composition that touches theme classes. NEVER concatenate strings with + or template literals.
Token Catalog
Exhaustive list of CSS custom properties shadcn ships in the default style, verified at https://ui.shadcn.com/docs/theming on 2026-05-19. Full type, purpose, and per-mode values in references/methods.md.
--radius is the base (default 0.625rem). Tailwind v4 derives :
--radius-sm: calc(var(--radius) * 0.6)
--radius-md: calc(var(--radius) * 0.8)
--radius-lg: var(--radius)
--radius-xl: calc(var(--radius) * 1.4)
--radius-2xl: calc(var(--radius) * 1.8)
--radius-3xl: calc(var(--radius) * 2.2)
--radius-4xl: calc(var(--radius) * 2.6)
ALWAYS expose these via @theme inline { --radius-sm: ...; --radius-md: ...; } so utilities like rounded-md resolve. NEVER hardcode rounded-[6px]; respect the token.
--destructive-foreground is NOT in the default catalog
ALWAYS pair color-foreground with its base (--card + --card-foreground, --primary + --primary-foreground). The default style ships --destructive WITHOUT a paired --destructive-foreground. Components that need destructive text use --foreground or a hardcoded white. Custom themes MAY add --destructive-foreground but it is not a default token. Verified at https://ui.shadcn.com/docs/theming 2026-05-19.
Decision Tree 1 : Setting up theming from scratch
Q1. Is the project Tailwind v4?
yes → Q2 (v4 path)
no → Q3 (v3 path)
Q2 (v4). globals.css MUST contain in this order :
1. @import "tailwindcss";
2. @import "tw-animate-css";
3. @custom-variant dark (&:is(.dark *));
4. :root { --background: oklch(...); ... } (oklch values, no @layer wrapper)
5. .dark { --background: oklch(...); ... } (oklch values, no @layer wrapper)
6. @theme inline { --color-background: var(--background); ... --radius-sm: calc(...); ... }
7. @layer base { * { @apply border-border outline-ring/50; } body { @apply bg-background text-foreground; } }
NO tailwind.config.js needed for colors. END.
Q3 (v3). Two files MUST be wired :
file 1 : globals.css
@tailwind base; @tailwind components; @tailwind utilities;
@layer base {
:root { --background: 0 0% 100%; ... } (HSL space-separated, NO hsl() wrapper, NO commas)
.dark { --background: 0 0% 3.9%; ... }
}
file 2 : tailwind.config.js
module.exports = {
darkMode: ["class"],
theme: { extend: { colors: { background: "hsl(var(--background))", ... }, borderRadius: { lg: "var(--radius)", ... } } },
plugins: [require("tailwindcss-animate")],
}
END.
Decision Tree 2 : Adding dark mode toggle
Q1. Is the project Next.js (App Router or Pages Router)?
yes → ALWAYS use next-themes :
1. npm i next-themes
2. components/theme-provider.tsx :
"use client"; export const ThemeProvider = (props) =>
<NextThemesProvider {...props}>{props.children}</NextThemesProvider>
3. app/layout.tsx :
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
{children}
</ThemeProvider>
</body>
</html>
4. components/mode-toggle.tsx : useTheme() + DropdownMenu with light/dark/system
END.
no → Q2
Q2. Is the project Vite + React?
yes → ALWAYS use the official shadcn Vite ThemeProvider (custom Context, localStorage,
NOT next-themes). Storage key default "vite-ui-theme". Identical mode-toggle.
END.
no → Astro / Remix / TanStack Start each have a dedicated page :
https://ui.shadcn.com/docs/dark-mode/{astro|remix|tanstack-start}
ALWAYS follow the framework-specific snippet ; the wrappers differ.
Q1. The theme builder outputs CSS for the project's Tailwind generation. Project on v4?
yes → copy the oklch block from the theme builder, paste into globals.css
replacing the existing :root and .dark blocks. KEEP the existing
@theme inline mapping and @custom-variant dark line; the builder
outputs only the variable values.
no → switch the theme builder's "format" toggle to HSL, then paste
the HSL block into globals.css inside @layer base.
KEEP the existing tailwind.config.js color mappings.
Patterns
Pattern 1 : The v3 wiring (HSL space-separated)
The historical (Tailwind v3.4) wiring uses HSL components stored as space-separated triples in CSS variables, then wrapped by hsl() at the consumption site (tailwind.config.js). The wrapper convention is the source of >40% of AI-generated theming bugs (verified anti-pattern, see references/anti-patterns.md).
ALWAYS in v3 :
Store : --background: 0 0% 100%; (three components, two spaces, no commas, no hsl())
--background: hsl(0, 0%, 100%); (commas + wrapper inside the variable)
--background: 0, 0%, 100%; (commas)
background: "var(--background)" (missing hsl() wrapper at the mapping site)
Full v3 globals.css and tailwind.config.js in references/examples.md.
Pattern 2 : The v4 wiring (oklch + @theme inline)
Tailwind v4 abandons tailwind.config.js for color theming and consumes variables directly via the @theme inline directive. The variables hold raw oklch() strings, the directive maps them into Tailwind's color namespace, and utilities like bg-background resolve automatically.
ALWAYS in v4 :
Store : --background: oklch(1 0 0); (full oklch() function call)
Use : bg-background (resolves via the @theme inline mapping)
NEVER in v4 :
--background: 0 0% 100%; (HSL legacy format; the inline mapping expects a color value)
bg-[hsl(var(--background))] (the wrapper convention from v3; works but defeats the point)
A tailwind.config.jscolors block (v4 ignores it for the new theming model)
Full v4 globals.css in references/examples.md.
Pattern 3 : The .dark class strategy
shadcn ships a class-based dark mode by default. The toggle (next-themes on Next.js, custom Context on Vite) adds or removes the .dark class on <html>. Both :root and .dark declare the SAME variable names with different values; cascade picks the closer match.
ALWAYS in v4 declare the variant : @custom-variant dark (&:is(.dark *)); (this is the v4 replacement for darkMode: ["class"]).
NEVER use @media (prefers-color-scheme: dark) for the variable overrides : it breaks the user-explicit toggle and produces inconsistency between system preference and explicit choice. next-themes already handles system mode by translating it into the class.
Pattern 4 : The next-themes ThemeProvider (Next.js)
The shadcn-distributed components/theme-provider.tsx is a four-line "use client" wrapper around next-themes's ThemeProvider. Verified verbatim at https://ui.shadcn.com/docs/dark-mode/next on 2026-05-19 :
ALWAYS pass all four props : attribute="class", defaultTheme="system", enableSystem, disableTransitionOnChange. ALWAYS keep suppressHydrationWarning on <html>. Removing any of these reintroduces hydration mismatch warnings or flash-of-unstyled-content. See references/anti-patterns.md for failure modes.
Pattern 5 : The Vite ThemeProvider (custom Context, NOT next-themes)
The Vite distribution uses a custom Context-based provider (no next-themes dependency). The storage key default is "vite-ui-theme". Functionally identical : light / dark / system, useTheme() hook, identical mode-toggle component. Full source in references/examples.md.
ALWAYS confirm the framework before installing : a Vite project that adds next-themes will work but adds a runtime dependency the shadcn docs do not prescribe. NEVER mix the two providers in the same project.
Pattern 6 : The mode-toggle component
The shadcn-distributed mode-toggle is a DropdownMenu with three items (Light / Dark / System) reading useTheme() and calling setTheme(...). Sun and Moon icons from lucide-react cross-fade with conditional Tailwind classes. ALWAYS compose its className with cn() ; NEVER concatenate.
Full mode-toggle component in references/examples.md.
Paste into the project's globals.css, replacing :root and .dark blocks ONLY
ALWAYS keep wiring intact (the @theme inline block in v4, the tailwind.config.js color block in v3). The theme builder outputs only variable values, NEVER the mapping layer. NEVER hand-edit individual oklch values without re-running the contrast check at the theme builder ; the foreground / background pairs are tuned for WCAG-AA.
Common Pitfalls (full details in references/anti-patterns.md)
NEVER do these. Each is verified from a published anti-pattern in the field :
NEVER use commas inside HSL variable values (0, 0%, 100%). The wrapper hsl(var(--background)) breaks silently; the output utility renders the literal string.
NEVER omit the hsl() wrapper in the v3 tailwind.config.js mapping. background: "var(--background)" produces an invalid color and Tailwind silently drops the utility.
NEVER mix v3 and v4 wiring : either tailwind.config.js colors OR @theme inline, never both. v4 ignores the JS config for theming; v3 ignores the directive.
NEVER store oklch values in a v3 project's variables; the hsl(var(--background)) wrapper at the mapping site will produce hsl(oklch(1 0 0)), which is invalid.
NEVER omit suppressHydrationWarning from <html> when using next-themes. React 18+ logs hydration mismatch warnings; first paint shows the wrong theme.
NEVER apply @media (prefers-color-scheme: dark) to override variables in a class-based shadcn setup. The class toggle and the media query conflict; the result depends on cascade order and confuses every reader.
Reference Links
references/methods.md : exhaustive CSS-variable catalog with type, purpose, light and dark defaults
references/examples.md : full globals.css for v3 and v4, both ThemeProvider files, the mode-toggle component, and a custom-palette override example