Implementing Dark and Light Theme Transitions in Cloned Websites: A Complete Guide

The ai-website-cloner-template uses CSS custom properties, a custom Tailwind dark variant, and a class-toggling mechanism on the root html element to enable seamless theme transitions in cloned Next.js 16 applications.

The ai-website-cloner-template repository provides a production-ready foundation for recreating websites with pixel-perfect fidelity. Its theming architecture leverages Next.js 16, Tailwind 4, and a utility-first approach to deliver smooth dark and light theme transitions that persist across user sessions.

How the Theming System Works

The template implements a three-layer theming strategy that separates design tokens from implementation logic.

CSS Custom Properties for Design Tokens

All color values, border radii, and font families are defined as CSS variables in src/app/globals.css. The file declares two distinct palettes: a :root block for light mode and a .dark block for dark mode. This variable-based approach ensures that any component can reference semantic names like --background or --foreground without hardcoding hex values.

Tailwind's Custom Dark Variant

Rather than relying on media queries alone, the template registers a custom variant in src/app/globals.css using the @custom-variant directive:

@custom-variant dark (&:is(.dark *));

This allows any Tailwind utility class to react to a parent .dark class, enabling explicit manual control over theme states while maintaining utility-first ergonomics.

Deterministic Class Merging with cn

The cn helper function in src/lib/utils.ts wraps clsx and tailwind-merge to produce deterministic class strings. When toggling themes dynamically, this prevents class conflicts and ensures the final CSS output remains predictable, especially when conditionally applying dark-mode utilities.

Step-by-Step Implementation

Implementing theme transitions requires coordinating CSS definitions, React state, and DOM manipulation.

Define Color Palettes in CSS Variables

Configure your design tokens in src/app/globals.css using CSS custom properties. Define light mode values in the :root selector and dark mode overrides in the .dark selector:

:root {
  --background: #ffffff;
  --foreground: #171717;
  --card: #ffffff;
  --card-foreground: #171717;
  --border: #e5e5e5;
}

.dark {
  --background: #0a0a0a;
  --foreground: #fafafa;
  --card: #0a0a0a;
  --card-foreground: #fafafa;
  --border: #262626;
}

Register the Tailwind Dark Variant

Add the custom variant declaration at the top of src/app/globals.css to enable the dark: modifier in your Tailwind classes:

@custom-variant dark (&:is(.dark *));

This registers the variant with Tailwind 4's engine, allowing utilities like dark:bg-background or dark:text-foreground to function correctly when the .dark class is present on a parent element.

Create the Theme Toggle Component

Build a React component that manages theme state and persists preferences to localStorage. The component toggles the .dark class on document.documentElement:

import { useEffect, useState } from "react";
import { cn } from "@/lib/utils";

export function ThemeToggle() {
  const [dark, setDark] = useState(() => {
    if (typeof localStorage !== "undefined") {
      return localStorage.getItem("theme") === "dark";
    }
    return false;
  });

  useEffect(() => {
    const html = document.documentElement;
    if (dark) {
      html.classList.add("dark");
      localStorage.setItem("theme", "dark");
    } else {
      html.classList.remove("dark");
      localStorage.setItem("theme", "light");
    }
  }, [dark]);

  return (
    <button
      onClick={() => setDark(!dark)}
      className={cn(
        "p-2 rounded transition-colors",
        dark ? "bg-muted-foreground text-muted" : "bg-muted text-muted-foreground"
      )}
      aria-label="Toggle dark / light theme"
    >
      {dark ? "🌙" : "☀️"}
    </button>
  );
}

Integrate with the Root Layout

The src/app/layout.tsx file renders the root <html> element, making it the ideal location to initialize theme logic. Import the ThemeToggle component and place it in your header or navigation area:

import { ThemeToggle } from "@/components/ThemeToggle";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" className="h-full antialiased">
      <body className="min-h-full flex flex-col">
        <header className="p-4 flex justify-end">
          <ThemeToggle />
        </header>
        {children}
      </body>
    </html>
  );
}

Using Theme-Aware Classes in Components

When building UI components, use semantic Tailwind classes that map to your CSS variables. The cn utility ensures safe class composition when conditionally applying styles:

import { cn } from "@/lib/utils";

export function Card({ children }: { children: React.ReactNode }) {
  return (
    <div
      className={cn(
        "rounded-lg p-6 shadow",
        "bg-card text-card-foreground",
        "border border-border"
      )}
    >
      {children}
    </div>
  );
}

Because bg-card and text-card-foreground reference CSS variables that change values between :root and .dark contexts, the component automatically adapts to the current theme without explicit dark-mode logic in the component code.

Summary

  • CSS variables in src/app/globals.css define light and dark color palettes under :root and .dark selectors.
  • Tailwind's custom variant (@custom-variant dark) enables the dark: prefix for utility classes.
  • DOM class toggling on the root <html> element triggers theme transitions and persists via localStorage.
  • The cn helper in src/lib/utils.ts provides deterministic class merging for dynamic theme classes.
  • Root layout integration in src/app/layout.tsx ensures the theme applies to the entire document tree.

Frequently Asked Questions

How does the theme preference persist across page reloads?

The theme toggle component initializes its state by checking localStorage for a stored "theme" key. When the user clicks the toggle, a useEffect hook updates both the DOM class and the storage value, ensuring the preference remains active across sessions and page navigations.

Why use a custom Tailwind variant instead of the media query strategy?

The @custom-variant dark (&:is(.dark *)); approach provides explicit manual control, allowing users to override system preferences. This is essential for cloned websites where designers may want to force a specific theme or provide a toggle independent of the operating system's dark mode setting.

What is the role of the cn utility function?

The cn function combines clsx for conditional class handling and tailwind-merge for conflicting class resolution. When implementing theme transitions, it ensures that dynamically constructed class strings (like those containing dark: variants) resolve to a single, predictable CSS output without duplicates or conflicting rules.

Can I use these CSS variables outside of Tailwind classes?

Yes. Because the colors are defined as standard CSS custom properties (--background, --foreground, etc.), you can reference them in inline styles, CSS modules, or even JavaScript via getComputedStyle. This flexibility allows custom components or third-party libraries to respect the theme even when not using Tailwind utilities.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →