Implementing Dark-to-Light Theme Transitions in Cloned Website Components

The ai-website-cloner-template leverages CSS custom properties, a custom Tailwind dark variant, and a root-level class toggle to enable instant dark-to-light theme transitions across cloned website components.

The JCodesMore/ai-website-cloner-template provides a Next.js 16 foundation for recreating websites with pixel-perfect fidelity. Its architecture combines Tailwind 4's custom variants with CSS custom properties to deliver a robust theming system that supports seamless dark-to-light theme transitions. By toggling a single dark class on the root HTML element, cloned components automatically adapt their color schemes without flash or layout shift.

How the Theming System Works

The template's approach rests on three interconnected pillars: CSS variables for design tokens, a custom Tailwind variant for dark mode detection, and a utility-first class merging helper.

CSS Custom Properties as Design Tokens

All visual values—colors, border radii, and font families—are stored as CSS variables in src/app/globals.css. The file defines a :root block for light mode values and a .dark block for dark mode overrides. Variables like --background, --foreground, and --card serve as the single source of truth, allowing components to reference semantic tokens rather than hardcoded hex codes.

Tailwind's Custom Dark Variant

Instead of relying on media queries, the template registers a custom variant in src/app/globals.css:

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

This enables any Tailwind utility to react to a parent .dark class. When the dark class is present on the <html> element, utilities like bg-background and text-foreground automatically resolve to the dark palette values defined in the CSS variables.

The cn Utility for Class Merging

Located in src/lib/utils.ts, the cn helper wraps clsx and tailwind-merge to produce deterministic class strings. This prevents style conflicts when conditionally applying theme-aware classes, ensuring that dark variants take precedence over light defaults during toggles.

Implementing the Theme Toggle

The theme toggle component manages state and persists preferences while manipulating the DOM directly. Since src/app/layout.tsx renders the <html> element, it serves as the anchor for the theme class.

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

export function ThemeToggle() {
  const [dark, setDark] = useState(() => {
    // Initialise from system or stored preference
    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>
  );
}

Persisting User Preferences

The component initializes from localStorage and updates the store on every change. Using useEffect, it synchronizes the dark class on document.documentElement with React state, ensuring the UI matches the persisted preference across page reloads.

Integrating the Toggle into the Root Layout

The root layout in src/app/layout.tsx is the ideal injection point for the theme toggle. By placing the toggle component in the header, users can switch modes instantly without navigating away from the current page.

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

Components consume the theme through semantic Tailwind classes mapped to CSS variables. The cn utility ensures clean class composition when mixing static and dynamic 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", // these map to CSS variables
        "border border-border"
      )}
    >
      {children}
    </div>
  );
}

Summary

  • The ai-website-cloner-template uses CSS custom properties in src/app/globals.css to define light and dark design tokens
  • A custom Tailwind variant (@custom-variant dark) enables class-based dark mode detection without media queries
  • The cn helper in src/lib/utils.ts ensures deterministic class merging during theme switches
  • Toggling the dark class on the root <html> element triggers instant transitions across all cloned components
  • User preferences persist via localStorage for consistent experiences across sessions

Frequently Asked Questions

How does the ai-website-cloner-template handle dark mode without using the dark: media query?

The template registers a custom Tailwind variant defined as @custom-variant dark (&:is(.dark *)); in src/app/globals.css. This creates a class-based selector that responds to a parent .dark class on the HTML element, allowing JavaScript to control the theme state rather than relying solely on system preferences.

Where should I place the theme toggle logic in a Next.js 16 app?

Place the theme state and toggle UI in a client component such as ThemeToggle.tsx, but inject the dark class on document.documentElement within that component or in src/app/layout.tsx. The root layout renders the <html> element, making it the canonical location for theme class management.

Why does the template use CSS custom properties instead of static Tailwind colors?

CSS custom properties (variables like --background) allow runtime switching without recompiling Tailwind classes. By storing values in src/app/globals.css under :root and .dark selectors, the template enables instant dark-to-light theme transitions and supports dynamic color values accessible via inline styles or standard CSS.

What is the purpose of the cn utility function in theme implementation?

The cn function in src/lib/utils.ts combines clsx for conditional class joining and tailwind-merge for deduplication. This ensures that when implementing dark-to-light theme transitions, conflicting classes resolve predictably, and the final class string always reflects the current theme state without duplicate or contradictory 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 →