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.cssto define light and dark design tokens - A custom Tailwind variant (
@custom-variant dark) enables class-based dark mode detection without media queries - The
cnhelper insrc/lib/utils.tsensures deterministic class merging during theme switches - Toggling the
darkclass on the root<html>element triggers instant transitions across all cloned components - User preferences persist via
localStoragefor 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →