shadcnthemes
All guides

Build a multi-theme switcher for shadcn/ui

Let users choose between several color themes, each with light and dark mode, using scoped CSS variables, a data-theme attribute and next-themes in Next.js.

Updated October 24, 2026

Skip the setup: use the theme switcher builder
Pick your themes and install them, plus a picker, hook and no-flash script, with one CLI command.

Light and dark mode is one axis. Many apps want a second: a choice of color themes (say "Ocean", "Forest" and "Rose"), each with its own light and dark palette. Since shadcn/ui reads everything from CSS variables, you can scope a full theme to an attribute and switch it at runtime without rebuilding anything.

1. Scope each theme to a data attribute

Copy the CSS for each theme you want from its page on this site, then rename the selectors. Keep your default theme on :root and .dark, and scope the extras:

/* Default theme */
:root { --primary: oklch(0.205 0 0); /* … */ }
.dark { --primary: oklch(0.922 0 0); /* … */ }

/* Ocean */
[data-theme="ocean"] { --primary: oklch(0.6 0.15 230); /* …all light tokens… */ }
.dark[data-theme="ocean"],
.dark [data-theme="ocean"] { --primary: oklch(0.72 0.13 230); /* …all dark tokens… */ }

/* Rose */
[data-theme="rose"] { --primary: oklch(0.62 0.2 10); /* … */ }
.dark[data-theme="rose"],
.dark [data-theme="rose"] { --primary: oklch(0.72 0.17 10); /* … */ }

The @theme inline block stays the same: it maps utilities to variables, and the variables now change with the attribute.

The .dark[data-theme] selector covers the usual setup where both the class and the attribute sit on <html>. The descendant version lets you scope a theme to part of the page, such as a preview pane.

2. Store the user's choice

Keep light/dark in next-themes as usual, and manage the color theme with a tiny hook that writes the attribute and remembers it:

"use client"

import { useEffect, useState } from "react"

export const COLOR_THEMES = ["default", "ocean", "rose"] as const
export type ColorTheme = (typeof COLOR_THEMES)[number]

export function useColorTheme() {
  const [theme, setTheme] = useState<ColorTheme>("default")

  useEffect(() => {
    const saved = localStorage.getItem("color-theme") as ColorTheme | null
    if (saved && COLOR_THEMES.includes(saved)) setTheme(saved)
  }, [])

  useEffect(() => {
    const root = document.documentElement
    if (theme === "default") root.removeAttribute("data-theme")
    else root.setAttribute("data-theme", theme)
    localStorage.setItem("color-theme", theme)
  }, [theme])

  return { theme, setTheme }
}

3. Avoid a flash on load

The hook runs after hydration, so a returning visitor would briefly see the default theme. Set the attribute before paint with a small inline script in your root layout:

<head>
  <script
    dangerouslySetInnerHTML={{
      __html: `try{var t=localStorage.getItem("color-theme");if(t&&t!=="default")document.documentElement.setAttribute("data-theme",t)}catch(e){}`,
    }}
  />
</head>

4. Build the picker

"use client"

import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { COLOR_THEMES, useColorTheme, type ColorTheme } from "@/hooks/use-color-theme"

export function ThemePicker() {
  const { theme, setTheme } = useColorTheme()
  return (
    <Select value={theme} onValueChange={(v) => setTheme(v as ColorTheme)}>
      <SelectTrigger className="w-36" aria-label="Color theme">
        <SelectValue />
      </SelectTrigger>
      <SelectContent>
        {COLOR_THEMES.map((t) => (
          <SelectItem key={t} value={t} className="capitalize">
            {t}
          </SelectItem>
        ))}
      </SelectContent>
    </Select>
  )
}

Ideas

  • Per-customer branding in a multi-tenant app: render data-theme={tenant.theme} on a wrapper on the server, with no client code at all.
  • Preview before applying: scope a theme to a card with data-theme, the same trick this site uses for its previews.
  • Fonts and radius can be scoped the same way, since they're variables too.

Find themes to offer in the gallery or the collections, or design your own in the editor.

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

More guides