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

  1. Use semantic color names in your Tailwind classes (e.g., text-primary instead of text-black).
  2. Test your components in both light and dark modes to ensure proper contrast and readability.
  3. Consider using CSS variables for complex theming beyond simple color changes.
  4. Use the useTheme hook from next-themes to 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.