Skip to main content

Prerequisites

Before installing themes, ensure you have shadcn/ui set up in your Vite React project. If you haven’t already:
1

Initialize shadcn/ui

npx shadcn@latest init
2

Install required components

The theme system requires these shadcn/ui components:
npx shadcn@latest add dropdown-menu scroll-area button

Installation

1

Install the theme system

Install the complete theme system with all 40+ themes:
npx shadcn@latest add https://tweakcn-picker.vercel.app/r/vite/theme-system
This installs:
  • lib/themes-config.ts - Theme configuration and metadata
  • components/theme-provider.tsx - Custom React theme provider
  • components/theme-switcher.tsx - Interactive theme switcher UI
  • styles/themes/*.css - All 40+ theme CSS files
2

Add ThemeProvider to your app root

Wrap your application with the ThemeProvider in src/App.tsx:
src/App.tsx
import { ThemeProvider } from "@/components/theme-provider";
import "@/styles/themes/index.css";

function App() {
  return (
    <ThemeProvider>
      {/* Your app content */}
    </ThemeProvider>
  );
}

export default App;
3

Add the ThemeSwitcher to your UI

Import and use the ThemeSwitcher component anywhere in your app:
src/components/Header.tsx
import { ThemeSwitcher } from "@/components/theme-switcher";

export function Header() {
  return (
    <header>
      <nav>
        {/* Your navigation */}
        <ThemeSwitcher />
      </nav>
    </header>
  );
}

How it works

The Vite adapter uses a custom theme provider with localStorage persistence:
Located at components/theme-provider.tsx:
import { createContext, useContext, useEffect, useState } from "react";
import { allThemeValues, DEFAULT_THEME } from "@/lib/themes-config";

type ThemeProviderState = {
  theme: string;
  setTheme: (theme: string) => void;
};

const ThemeProviderContext = createContext<ThemeProviderState>({
  theme: DEFAULT_THEME,
  setTheme: () => null,
});

export function ThemeProvider({
  children,
  defaultTheme = DEFAULT_THEME,
  storageKey = "tweakcn-theme",
}: {
  children: React.ReactNode;
  defaultTheme?: string;
  storageKey?: string;
}) {
  const [theme, setTheme] = useState<string>(
    () => localStorage.getItem(storageKey) || defaultTheme,
  );

  useEffect(() => {
    const root = window.document.documentElement;
    root.setAttribute("data-theme", theme);
  }, [theme]);

  const value = {
    theme,
    setTheme: (theme: string) => {
      localStorage.setItem(storageKey, theme);
      setTheme(theme);
    },
  };

  return (
    <ThemeProviderContext.Provider value={value}>
      {children}
    </ThemeProviderContext.Provider>
  );
}

export const useTheme = () => {
  const context = useContext(ThemeProviderContext);
  if (context === undefined)
    throw new Error("useTheme must be used within a ThemeProvider");
  return context;
};
The provider manages theme state and automatically updates the data-theme attribute on the document root.
All theme metadata is in lib/themes-config.ts:
export interface ThemeConfig {
  name: string;          // e.g., "catppuccin"
  title: string;         // Display name
  primaryLight: string;  // Primary color in light mode
  primaryDark: string;   // Primary color in dark mode
  fontSans: string;      // Font family
}

export const themes: ThemeConfig[] = [
  {
    name: "vercel",
    title: "Vercel",
    primaryLight: "oklch(0 0 0)",
    primaryDark: "oklch(1 0 0)",
    fontSans: "Geist, sans-serif",
  },
  // ... 40+ more themes
];

// Generate all theme values (name-light and name-dark)
export const allThemeValues = themes.flatMap((t) => [
  `${t.name}-light`,
  `${t.name}-dark`,
]);
Themes are automatically saved to localStorage with the key tweakcn-theme:
const setTheme = (theme: string) => {
  localStorage.setItem(storageKey, theme);
  setTheme(theme);
};
On page load, the theme is restored from localStorage:
const [theme, setTheme] = useState<string>(
  () => localStorage.getItem(storageKey) || defaultTheme,
);

Adding individual themes

To install specific themes instead of all 40+:
npx shadcn@latest add https://tweakcn-picker.vercel.app/r/theme-vercel
Then import only the themes you need:
src/styles/themes/index.css
@import "./vercel.css";
@import "./github.css";
@import "./stripe.css";

Customizing themes

All theme CSS files are in src/styles/themes/. Each theme defines CSS variables:
src/styles/themes/vercel.css
[data-theme="vercel-light"] {
  --background: oklch(1 0 0);
  --foreground: oklch(0 0 0);
  --primary: oklch(0 0 0);
  --primary-foreground: oklch(1 0 0);
  /* ... more variables */
}

[data-theme="vercel-dark"] {
  --background: oklch(0 0 0);
  --foreground: oklch(1 0 0);
  --primary: oklch(1 0 0);
  --primary-foreground: oklch(0 0 0);
  /* ... more variables */
}
Edit these files to customize colors, borders, shadows, and more.

TypeScript usage

Use the useTheme hook from the theme provider:
import { useTheme } from "@/components/theme-provider";

function MyComponent() {
  const { theme, setTheme } = useTheme();
  
  return (
    <div>
      <p>Current theme: {theme}</p>
      <button onClick={() => setTheme("vercel-dark")}>
        Switch to Vercel Dark
      </button>
      <button onClick={() => setTheme("github-light")}>
        Switch to GitHub Light
      </button>
    </div>
  );
}

Vite configuration

Ensure your vite.config.ts has path aliases configured:
vite.config.ts
import path from "path";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
});

Next steps

Browse Themes

Explore all 40+ available themes

Theme Picker

Preview themes in real-time

Next.js Setup

Install themes in Next.js

Remix Setup

Install themes in Remix

Build docs developers (and LLMs) love