shadcnthemes
All guides

Add dark mode to shadcn/ui in Next.js

Set up light, dark and system themes for shadcn/ui in a Next.js App Router project with next-themes, avoid the flash of the wrong theme, and build a mode toggle.

Updated October 24, 2026

shadcn/ui's dark mode is just CSS: your theme's .dark block overrides the light values whenever the dark class is present on a parent element. In Next.js, the cleanest way to manage that class is next-themes, which handles system preference, persistence and hydration for you.

1. Install next-themes

npm install next-themes

2. Add a theme provider

Create a small client component that wraps next-themes:

"use client"

import { ThemeProvider as NextThemesProvider } from "next-themes"

export function ThemeProvider(props: React.ComponentProps<typeof NextThemesProvider>) {
  return <NextThemesProvider {...props} />
}

3. Wrap your root layout

import { ThemeProvider } from "@/components/theme-provider"

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body>
        <ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
          {children}
        </ThemeProvider>
      </body>
    </html>
  )
}
  • attribute="class" puts dark on <html>, which is what shadcn's @custom-variant dark (&:is(.dark *)) expects.
  • suppressHydrationWarning is needed because next-themes sets the class before React hydrates, which is what prevents a flash of the wrong theme.
  • disableTransitionOnChange stops every color transition from animating at once when the theme flips.

4. Add a mode toggle

"use client"

import { Moon, Sun } from "lucide-react"
import { useTheme } from "next-themes"

import { Button } from "@/components/ui/button"

export function ModeToggle() {
  const { resolvedTheme, setTheme } = useTheme()
  return (
    <Button
      variant="ghost"
      size="icon"
      aria-label="Toggle dark mode"
      onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}
    >
      <Sun className="scale-100 rotate-0 transition dark:scale-0 dark:-rotate-90" />
      <Moon className="absolute scale-0 rotate-90 transition dark:scale-100 dark:rotate-0" />
    </Button>
  )
}

The icons switch with CSS dark: variants rather than JavaScript, so the right icon shows even before hydration.

Designing a good dark palette

Dark mode isn't light mode inverted. Some guidelines:

  • Use a very dark gray, not pure black, for the background (around oklch(0.14–0.18 …)), and make cards slightly lighter than the page so surfaces separate.
  • Lighten and slightly desaturate your primary so it doesn't vibrate against the dark background.
  • Use translucent borders such as oklch(1 0 0 / 10%), which adapt to whatever surface they sit on.
  • Check contrast again: muted text that passes in light mode often fails in dark mode.

Every theme on this site ships a tuned dark palette. Browse them all previewed in dark mode, or adjust one in the editor with the light/dark toggle.

Open the theme editor
Edit every token in light and dark, check contrast, and export Tailwind v4 CSS.

More guides