Shadcn Dark Mode Fix That Actually Works
· Tutorials
The exact configuration for shadcn dark mode with next-themes — CSS variables, system preference detection, and FOUC prevention in one pass.
Last updated: July 24, 2026 · 4-minute read
I lost 4 hours to a single missing attribute in my root layout. The CSS variables were right, the Tailwind config was right, the ThemeProvider was imported correctly — everything looked fine. Except dark mode kept flickering on load or ignoring system preference entirely. This is the fix that actually resolved it.
The Problem
Shadcn ships with dark mode support built into its CSS variable system, but it does not handle the toggle logic or the initialization. That is on you. The docs mention next-themes in passing, but the actual wiring requires three specific pieces that are easy to get wrong.
A thread on r/shadcnui from June 2026 had 340 upvotes on a post titled "Dark mode just doesn't work" — and the fix was scattered across 47 comments. Here it is in one place.
Step 1: The ThemeProvider Setup
Install next-themes and wrap your app in the provider. The critical part is the attribute and defaultTheme props.
export function ThemeProvider({ children }: { children: React.ReactNode }) { return ( <NextThemesProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange {children} </NextThemesProvider ); }
The attribute="class" tells next-themes to toggle the dark class on the HTML element, which is what Tailwind's darkMode: "class" expects. The disableTransitionOnChange prop prevents a fade animation between themes that can look janky.
Step 2: Prevent the Flash of Unstyled Content
This is the part that cost me 4 hours. Without a client-side script that runs before React hydrates, the page renders in light mode for 200-400ms even if the user has system dark mode enabled.
Add this script tag inside the <head of your root layout, before any other scripts:
This reads the stored theme preference (or falls back to system preference) and applies the dark class synchronously. No flash, no FOUC. Someone on r/tailwindcss pointed out that dangerouslySetInnerHTML is the only reliable way to do this in Next.js App Router since inline scripts in <Script tags can still flash.
Step 3: The Toggle Component
Shadcn has a built-in theme toggle example, but the version in the CLI as of v2.4 still references a deprecated API. Here is the corrected version:
export function ThemeToggle() { const { setTheme, resolvedTheme } = useTheme();
return ( <button onClick={() = setTheme(resolvedTheme === "dark" ? "light" : "dark")} className="inline-flex h-9 w-9 items-center justify-center rounded-md" <Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" / <Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" / </button ); }
Use resolvedTheme instead of theme. The theme value can be "system", which does not help you determine the current visual state. resolvedTheme resolves "system" to either "light" or "dark" based on the actual computed preference.
Step 4: Tailwind Config Verification
Make sure your tailwind.config.ts has darkMode: "class". Shadcn's init command sets this up, but if you are migrating an existing project, double-check. The default Tailwind behavior uses media (system preference), which conflicts with next-themes' class-based approach.
This is the exact setup I use on this site. More frontend tutorials are in the blog.
---
Not affiliated with shadcn or next-themes. Tools used: Next.js 16, Tailwind CSS 4, shadcn/ui, VS Code.