fundamentals

Vite Theme Integration

Add dark mode to your Vite application

Create a Theme Provider

components/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 initialState: ThemeProviderState = {
	theme: "system",
	setTheme: () => null,
}
 
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
 
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])
 
	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 Root Layout

Add the ThemeProvider to your root layout.

App.tsx
import { ThemeProvider } from "@/components/theme-provider"
 
function App() {
	return (
		<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
			{children}
		</ThemeProvider>
	)
}
 
export default App

Create a Theme Toggler

components/theme-toggler.tsx
import { useEffect, useState } from "react"
import { Loader2, MoonIcon, SunIcon } from "lucide-react"
import { useTheme } from "@/components/theme-provider"
import { Button, IconButton } from "@/components/ui/button"
 
export function ThemeToggler() {
	const [mounted, setMounted] = useState(false)
	const { theme, setTheme } = useTheme()
 
	const toggleTheme = () => {
		setTheme(theme === "light" ? "dark" : "light")
	}
 
	useEffect(() => {
		setMounted(true)
	}, [])
 
	if (!mounted) {
		return (
			<IconButton variant="outline" color="neutral" disabled>
				<Loader2 className="size-5 animate-spin" />
			</IconButton>
		)
	}
 
	return (
		<IconButton variant="outline" color="neutral" onClick={toggleTheme}>
			{theme === "light" ? <MoonIcon /> : <SunIcon />}
		</IconButton>
	)
}

Use the Theme Toggler

components/navigation-bar.tsx
import { ThemeToggler } from "@/components/theme-toggler"
 
export function NavigationBar() {
	return (
		<nav>
			// ....
			<ThemeToggler />
		</nav>
	)
}