Dark Mode
Follow this comprehensive guide to add dark mode support to your project using VizStats UI components. Our implementation is based on the shadcn/ui approach, providing a seamless and customizable dark mode experience.
Prerequisites
Ensure you have VizStats UI installed in your project. If not, follow the installation guide.
Step 1: Install Dependencies
Install the necessary dependencies using your preferred package manager:
npm install next-themes
Step 2: Set Up the Theme Provider
Create a new file called theme-provider.tsx in your components directory:
components/theme-provider.tsx
"use client";
import * as React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
export function ThemeProvider({
children,
...props
}: React.ComponentProps<typeof NextThemesProvider>) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}
Then, wrap your app with the ThemeProvider in your layout.tsx file:
import { ThemeProvider } from '@/components/theme-provider'
export default function RootLayout({ children }: RootLayoutProps) {
return (
<html lang="en" suppressHydrationWarning>
<head />
<body>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
</body>
</html>
);
}
Step 3: Create a Theme Toggle
Create a new component called theme-toggle.tsx
Mode Toggle
Click the toggle to switch between light, dark, and system themes
import { useTheme } from "next-themes";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Moon, Sun } from "lucide-react";
export function ModeToggle() {
const { setTheme } = useTheme();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon">
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="center">
<DropdownMenuItem onClick={() => setTheme("light")}>
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
ModeSwitch
Click to switch between light and dark modes
import { useTheme } from "next-themes";
import { useState, useEffect } from "react";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Moon, Sun } from "lucide-react";
export function ModeSwitch() {
const [mounted, setMounted] = useState(false);
const { theme, setTheme } = useTheme();
useEffect(() => {
setMounted(true);
}, []);
const toggleTheme = () => {
setTheme(theme === "dark" ? "light" : "dark");
};
if (!mounted) return null;
return (
<div>
<div className="relative inline-grid h-9 grid-cols-[1fr_1fr] items-center text-sm font-medium">
<Switch
id="theme-switch"
checked={theme === "dark"}
onCheckedChange={toggleTheme}
className="peer absolute inset-0 h-[inherit] w-auto dark:bg-input/50 bg-input/50 [&_span]:h-full [&_span]:w-1/2 [&_span]:transition-transform [&_span]:duration-300 [&_span]:[transition-timing-function:cubic-bezier(0.16,1,0.3,1)] dark:[&_span]:translate-x-full rtl:dark:[&_span]:-translate-x-full"
/>
<span className="pointer-events-none relative ms-0.5 flex min-w-8 items-center justify-center text-center dark:text-muted-foreground/70">
<Sun size={16} strokeWidth={2} aria-hidden="true" />
</span>
<span className="pointer-events-none relative me-0.5 flex min-w-8 items-center justify-center text-center dark:text-white text-muted-foreground/70">
<Moon size={16} strokeWidth={2} aria-hidden="true" />
</span>
</div>
<Label htmlFor="theme-switch" className="sr-only">
Toggle Theme
</Label>
</div>
);
}
ModeButton
"use client";
import { useTheme } from "next-themes";
import { useState, useEffect } from "react";
import { Moon, Sun } from "lucide-react";
import { motion } from "framer-motion";
export function ModeButton() {
const { theme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
const toggleTheme = () => {
setTheme(theme === "dark" ? "light" : "dark");
};
if (!mounted) return null;
return (
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={toggleTheme}
className="relative flex items-center justify-center p-2 rounded-lg bg-muted text-gray-800 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-muted/80 transition-colors"
aria-label="Toggle theme"
>
{/* Sun Icon */}
<Sun className="h-5 w-5 transition-transform duration-300 dark:rotate-90 dark:scale-0" />
{/* Moon Icon */}
<Moon className="absolute h-5 w-5 transition-transform duration-300 rotate-90 scale-0 dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</motion.button>
);
}
Step 4: Use the Theme Toggle
Add the ThemeToggle component to your layout or navbar:
import { ModeToggle } from '@/components/theme-toggle'
export function Header() {
return (
<header>
{/* Other header content */}
<ModeToggle />
</header>
)
}
Best Practices
- Use semantic color names in your Tailwind classes (e.g.,
text-primaryinstead oftext-black). - Test your components in both light and dark modes to ensure proper contrast and readability.
- Consider using CSS variables for complex theming beyond simple color changes.
- Use the
useThemehook fromnext-themesto programmatically access or change the current theme.
Conclusion
By following these steps, you've successfully added dark mode support to your project using VizStats UI. Your users can now toggle between light, dark, and system themes for a personalized experience.