Use when building or customizing a theme in a shadcn ui project, wiring the dark mode toggle, replacing the default primary color with a brand color, copying the output of the ui.shadcn.com themes builder into globals.css, or overriding the styling of one specific component instance without touching the global tokens. This is the end-to-end workflow recipe : the token catalog, oklch versus HSL format split, and v3 versus v4 wiring rules live in shadcn-core-theming. Prevents the flash-of-unstyled-content on first paint that comes from a missing suppressHydrationWarning, the silent "bg-primary did not change" symptom that comes from editing tokens without re-running the dev server, the two-ThemeProvider double-mount that breaks the toggle, the next-themes attribute mismatch that disables Tailwind's dark variants, and the brand-color contrast trap that ships an inaccessible palette. Covers the three-step theme-builder workflow (pick base style and radius, pick primary, copy CSS), the Tailwind v3 globals.css plus
Instalar com Codex ou Claude Copie este prompt, cole no Codex, Claude ou outro assistente e deixe que ele revise a página da skill e instale para você.
Um comando direto ignora o prompt de revisão. Verifique a origem antes de executá-lo.
Use when building or customizing a theme in a shadcn ui project, wiring the dark mode toggle, replacing the default primary color with a brand color, copying the output of the ui.shadcn.com themes builder into globals.css, or overriding the styling of one specific component instance without touching the global tokens. This is the end-to-end workflow recipe : the token catalog, oklch versus HSL format split, and v3 versus v4 wiring rules live in shadcn-core-theming. Prevents the flash-of-unstyled-content on first paint that comes from a missing suppressHydrationWarning, the silent "bg-primary did not change" symptom that comes from editing tokens without re-running the dev server, the two-ThemeProvider double-mount that breaks the toggle, the next-themes attribute mismatch that disables Tailwind's dark variants, and the brand-color contrast trap that ships an inaccessible palette. Covers the three-step theme-builder workflow (pick base style and radius, pick primary, copy CSS), the Tailwind v3 globals.css plus tailwind.config.js paste-target, the Tailwind v4 single-file globals.css paste-target with the @theme inline mapping, the Next.js ThemeProvider mount on app/layout.tsx with the four required props, the Vite custom Context ThemeProvider mount on main.tsx, the ModeToggle component with light/dark/system items, the brand-color override pattern keeping the foreground pair in sync, the custom palette build path with WCAG-AA verification, and the per-component override via the data-slot attribute that ships on every v4 primitive. Keywords: custom theme shadcn, brand theme, brand color shadcn, dark mode toggle, dark mode shadcn, next-themes, theme provider, ThemeProvider, useTheme, mode-toggle component, ModeToggle, theme builder, ui.shadcn.com themes, how do I customize colors, how do I change the primary color, how do I add dark mode, theme flash unstyled content, FOUC, flash of unstyled content, hydration mismatch theme, suppressHydrationWarning, shadcn brand color, custom palette, per-component override shadcn, data-slot, data-slot card, oklch theme, HSL theme, paste theme CSS, globals.css theme, replace primary color, override token shadcn, ThemeProvider attribute class, system theme preference, light dark system toggle, useTheme hook, setTheme function, dark variant not working, my theme did not change, theme not applying, lucide Sun Moon icons toggle, DropdownMenu mode toggle, double ThemeProvider nested provider error.
license
MIT
compatibility
Designed for Claude Code. Requires shadcn ui evergreen-2026.
metadata
{"author":"OpenAEC-Foundation","version":"1.0"}
shadcn ui : Theming Custom (Workflow Recipe)
End-to-end recipe for building, pasting, and wiring a custom theme. This skill is the workflow ; the token mental model (oklch versus HSL, v3 versus v4 wiring, full token catalog, decision trees) lives in shadcn-core-theming. ALWAYS read that skill first when in doubt about a token format or wiring location.
Quick Reference : The three-step happy path
The shortest path from "we want a custom theme" to a working dark-mode-toggling app :
Pick : open https://ui.shadcn.com/themes, pick a base style (default, new-york, sera, luma), pick a radius (0.0rem, 0.25rem, 0.5rem, 0.75rem, 1.0rem), pick a primary color (Zinc, Slate, Stone, Gray, Neutral, Red, Rose, Orange, Green, Blue, Yellow, Violet).
Copy : click "Copy code". The builder emits a single CSS block containing :root { ... } and .dark { ... } blocks, formatted for the project's Tailwind generation (oklch for v4, HSL space-separated for v3).
Paste : open app/globals.css (Next.js) or src/index.css (Vite). Replace the existing :root { ... } and .dark { ... } blocks ONLY. Keep the @import "tailwindcss"; line, the @custom-variant dark line, the @theme inline { ... } block, and the @layer base { ... } block intact.
That is the ENTIRE happy path. If the project already has the ThemeProvider mounted, the new colors take effect on the next dev-server reload. If not, follow the framework-specific mount recipe below.
Five invariants
ALWAYS paste only the :root and .dark blocks from the theme builder ; NEVER paste the @theme inline mapping (it already lives in the project).
ALWAYS verify the builder output matches the project's Tailwind generation (oklch for v4, HSL space-separated for v3) BEFORE pasting. Wrong format = silent zero-style output.
ALWAYS mount exactly ONE ThemeProvider at the root layout. Nesting two providers (e.g. next-themes inside a custom Context) produces unpredictable toggle behavior.
ALWAYS pass to next-themes ; any other value (e.g. ) disables Tailwind's variant in shadcn projects because the selector targets a class, not an attribute.
attribute="class"
attribute="data-theme"
dark:
.dark
ALWAYS put suppressHydrationWarning on <html> when using next-themes in Next.js. Omitting it produces a hydration mismatch warning and a first-paint flash of the wrong theme.
Decision Tree 1 : Which workflow do I need?
Q1. Goal = "use a built-in palette" (the theme builder output is enough)?
yes -> Workflow A : theme-builder paste (below)
no -> Q2
Q2. Goal = "override one specific token" (e.g. brand primary, brand radius)?
yes -> Workflow B : surgical token override (below)
no -> Q3
Q3. Goal = "build a custom palette from a brand color"?
yes -> Workflow C : custom palette build (below)
no -> Q4
Q4. Goal = "style one component instance differently"?
yes -> Workflow D : per-component override via data-slot (below)
no -> NOT a theming task. Check shadcn-core-theming for token semantics
or shadcn-impl-component-install for adding components.
Workflow A : Theme-builder paste
The fastest custom theme. Total time : approximately 60 seconds.
1. Navigate to https://ui.shadcn.com/themes.
2. In the right panel : pick Style (default / new-york / sera / luma).
3. In the right panel : pick Color (Zinc / Slate / Stone / Gray / Neutral /
Red / Rose / Orange / Green / Blue / Yellow / Violet).
4. In the right panel : pick Radius (0.0 / 0.25 / 0.5 / 0.75 / 1.0).
5. Click "Copy code" at the top-right of the right panel.
6. Open the project's main CSS file :
- Next.js : app/globals.css (or src/app/globals.css in monorepos)
- Vite + React : src/index.css
- Astro / Remix : project-specific ; usually src/styles/globals.css
7. Locate the existing :root { ... } and .dark { ... } blocks.
8. Replace them with the pasted blocks ONLY.
9. Save. The dev server hot-reloads ; the new theme is live.
ALWAYS preserve everything ELSE in globals.css : the @import "tailwindcss"; line, the @import "tw-animate-css"; line, the @custom-variant dark (&:is(.dark *)); line, the @theme inline { --color-background: var(--background); ... } mapping block, and the @layer base { ... } block. The builder output is variable VALUES only ; the wiring layer stays unchanged. Full globals.css reference in references/examples.md.
Format-verification check
Before pasting, glance at the first variable line. If it reads --background: oklch(1 0 0); the format is v4. If it reads --background: 0 0% 100%; the format is v3. Cross-check against the project : package.json"tailwindcss": "^4..." is v4 ; "^3..." is v3. NEVER paste v4 (oklch) values into a v3 project ; the hsl(var(--background)) wrapper at the mapping site produces hsl(oklch(...)), which is invalid and silently drops the utility.
The theme builder has a format toggle in the right panel for projects still on v3. Switch it BEFORE clicking "Copy code".
Workflow B : Surgical token override
When the goal is "use the default shadcn theme but change ONE thing" (e.g. corporate brand primary), edit ONLY the relevant variables in :root and .dark. Both blocks MUST be updated in lockstep.
Brand-primary override (v4 example)
:root {
/* ... all other shadcn defaults unchanged ... */--primary: oklch(0.4880.243264.376); /* brand purple */--primary-foreground: oklch(0.98500); /* near-white text */
}
.dark {
/* ... all other shadcn defaults unchanged ... */--primary: oklch(0.6920.180270.000); /* lighter brand purple for dark mode */--primary-foreground: oklch(0.20500); /* near-black text */
}
ALWAYS update BOTH :root AND .dark. The default theme tunes them for opposite contrast ; overriding only :root leaves .dark showing the original shadcn primary, producing a brand-mismatch in dark mode.
ALWAYS keep --primary-foreground in sync. The pair is contrast-tuned. A brand-purple primary with a --primary-foreground of pure black fails WCAG-AA. Run the chosen pair through https://webaim.org/resources/contrastchecker/ before shipping.
Brand-radius override
:root {
--radius: 0.75rem; /* override base radius */
}
The @theme inline block derives --radius-sm through --radius-4xl from --radius ; no per-step override needed. NEVER hardcode rounded-[12px] in components ; respect the token.
Workflow C : Custom palette build
When the theme builder palettes do not match the brand, build from scratch.
1. Identify the brand primary color in any color space (hex / RGB / HSL).
2. Convert to oklch (v4) or HSL space-separated (v3). See references/methods.md
for conversion guidance.
3. Derive the foreground pair : near-white if the primary is dark,
near-black if the primary is light. Target WCAG-AA contrast.
4. Optionally derive accent, secondary, muted, destructive from the
brand palette. ALWAYS keep the foreground pair in sync per token.
5. Write both :root { ... } and .dark { ... } blocks. For each token,
the .dark value is typically lighter than :root for surface tokens
(background, card, popover, muted) and inverted for the foreground.
6. Paste into globals.css per Workflow A step 6+.
ALWAYS verify contrast for every foreground-pair token (primary + primary-foreground, secondary + secondary-foreground, etc.) in BOTH :root and .dark. A 5-pair palette is a 20-check matrix. Full palette template in references/examples.md.
ALWAYS start with the default shadcn palette as a baseline ; modify the 3-5 tokens that need to change. NEVER rewrite all 19 core tokens from scratch unless the brand mandates it ; the default is contrast-tuned.
Workflow D : Per-component override via data-slot
Every shadcn primitive in v4 ships with a data-slot attribute on each subcomponent (e.g. Card has data-slot="card", CardHeader has data-slot="card-header", AccordionTrigger has data-slot="accordion-trigger"). This is the canonical hook for styling ONE specific component without touching the global tokens.
Pattern : Tailwind arbitrary variant
// Override Card padding for a specific instance using a wrapper class
<div className="[&_[data-slot=card]]:p-8 [&_[data-slot=card-header]]:pb-2">
<Card><CardHeader>...</CardHeader><CardContent>...</CardContent></Card>
</div>
Pattern : Global CSS-selector override
/* In globals.css, scoped to a wrapper class */.brand-pricing[data-slot=card] {
border-width: 2px;
border-color: var(--primary);
}
ALWAYS prefer data-slot selectors over component className overrides when the goal is a project-wide tweak to a specific subcomponent. The data-slot attribute is stable across shadcn add --overwrite cycles ; an internal className like rounded-xl may shift between versions.
ALWAYS scope the override under a wrapper class (.brand-pricing, [data-section=hero], etc.) to avoid affecting every Card in the app. A bare [data-slot=card] { ... } rule applies globally.
Full override examples in references/examples.md.
Decision Tree 2 : Wiring the dark-mode toggle
Q1. Is the project Next.js (App Router or Pages Router)?
yes -> Workflow E : next-themes mount (below)
no -> Q2
Q2. Is the project Vite + React?
yes -> Workflow F : custom Context mount (below)
no -> Astro / Remix / TanStack Start : framework-specific page at
https://ui.shadcn.com/docs/dark-mode/{astro|remix|tanstack-start}
ALWAYS pass all four props. ALWAYS keep suppressHydrationWarning on <html>. ALWAYS put "use client" at the top of theme-provider.tsx ; next-themes reads localStorage and window.matchMedia, both client-only APIs.
Full prop semantics and useTheme signature in references/methods.md.
Workflow F : Custom Context mount (Vite)
The Vite distribution uses a custom React Context provider, NOT next-themes. Storage key default "vite-ui-theme". Full source verbatim at https://ui.shadcn.com/docs/dark-mode/vite on 2026-05-19.
Step 1 : create src/components/theme-provider.tsx
See full source code in references/examples.md. The provider reads localStorage, applies the .light or .dark class to document.documentElement, and exports a useTheme() hook.
ALWAYS pick a stable storageKey per app. NEVER share storage keys across apps on the same domain ; the toggle would propagate globally and confuse users.
NEVER install next-themes in a Vite project. It works but adds an unnecessary runtime dependency and breaks parity with the official shadcn docs.
Pattern : The ModeToggle component
A single DropdownMenu with three items (Light / Dark / System) reading useTheme() and calling setTheme(...). Sun and Moon icons from lucide-react cross-fade via dark: Tailwind variants.
ALWAYS import useTheme from next-themes in Next.js projects, from @/components/theme-provider in Vite projects. They have the same surface ({ theme, setTheme, ... }) but are different modules.
ALWAYS include the System item ; defaulting to system preference is the documented shadcn behavior and respects OS-level user choice.
ALWAYS add "use client" at the top in Next.js : the component calls useTheme() which reads client-only state.
Common Pitfalls (full details in references/anti-patterns.md)
NEVER do these. Each is verified from documented anti-patterns and field experience :
NEVER ship a Next.js app without suppressHydrationWarning on <html>. The server renders without the theme class ; the client adds it ; React 18+ logs a hydration mismatch and the first paint flashes the wrong theme.
NEVER write HSL comma-separated values in a v3 project (--background: 0, 0%, 100%). The hsl(var(--background)) wrapper breaks silently ; the utility renders the literal string.
NEVER paste oklch values into a v3 project. The wrapper produces hsl(oklch(...)) which is invalid CSS and the utility is dropped.
NEVER pass attribute="data-theme" to next-themes in a shadcn project. The default style ships .dark as a class selector ; attribute="data-theme" adds a data-theme="dark" attribute that the .dark selector and Tailwind's dark: variant ignore.
NEVER nest two ThemeProviders. A common bug : adding a custom Context provider for app-state on top of next-themes, then nesting next-themes again at a layout boundary. The toggle becomes unpredictable.
Reference Links
references/methods.md : next-themes ThemeProvider prop reference, useTheme hook signatures (Next.js and Vite), oklch and HSL conversion guidance, palette-generation matrix
references/examples.md : full v4 globals.css with light + dark + brand-primary override, Next.js app/layout.tsx ThemeProvider mount, Vite App.tsx + custom ThemeContext, ModeToggle component, per-component data-slot override