Vite

Setting up dark mode in your Vite project.

Create a theme provider component.

providers/theme-provider.tsx
import { createContext, useContext, useEffect, useState } from "react"
 
type Theme = "dark" | "light" | "system"
 
type ThemeProviderProps = {
	children: React.ReactNode
	defaultTheme?: Theme
	storageKey?: string
}
 
type ThemeProviderState = {
	theme: Theme
	setTheme: (theme: Theme) => void
}
 
const ThemeProviderContext = createContext<ThemeProviderState | undefined>(
	undefined
)
 
export function ThemeProvider({
	children,
	defaultTheme = "system",
	storageKey = "vite-ui-theme",
	...props
}: ThemeProviderProps) {
	const [theme, setTheme] = useState<Theme>(
		() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
	)
 
	useEffect(() => {
		const root = window.document.documentElement
 
		root.classList.remove("light", "dark")
 
		if (theme === "system") {
			const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
				.matches
				? "dark"
				: "light"
			root.classList.add(systemTheme)
			return
		}
 
		root.classList.add(theme)
	}, [theme])
 
	useEffect(() => {
		const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
 
		// Add listener for system theme changes
		const handleChange = () => {
			if (theme === "system") {
				const root = window.document.documentElement
				root.classList.remove("light", "dark")
				root.classList.add(mediaQuery.matches ? "dark" : "light")
			}
		}
 
		mediaQuery.addEventListener("change", handleChange)
		return () => mediaQuery.removeEventListener("change", handleChange)
	}, [theme])
 
	const value = {
		theme,
		setTheme: (theme: Theme) => {
			localStorage.setItem(storageKey, theme)
			setTheme(theme)
		},
	}
 
	return (
		<ThemeProviderContext.Provider {...props} 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
}

Wrap your app with the provider.

main.tsx
import { ThemeProvider } from "@/providers/theme-provider"
 
function App() {
	return (
		<ThemeProvider defaultTheme="system">
			{/* Your app content */}
		</ThemeProvider>
	)
}
 
export default App

Add theme toggle component.

components/theme-toggle.tsx
import * as React from "react"
import { MoonIcon, SunIcon } from "lucide-react"
 
import { Button } from "@/components/ui/button"
 
import { useTheme } from "@/providers/theme-provider"
 
export default function ThemeToggle() {
	const { setTheme, theme } = useTheme()
 
	const toggleTheme = React.useCallback(() => {
		setTheme(theme === "dark" ? "light" : "dark")
	}, [theme, setTheme])
 
	return (
		<Button onClick={toggleTheme} variant="ghost" size="icon">
			<MoonIcon className="dark:hidden" />
			<SunIcon className="hidden dark:block" />
			<span className="sr-only">Toggle theme</span>
		</Button>
	)
}