I had some trouble getting gemini to implement themes for my application so I thought I would share this with others to save them some problems. First ask gemini to implement a Light/Dark theme switcher. After that is completed just copy and paste the document contained below into the chat window to get these themes setup.
The person who created this work has dedicated the work to the public domain by waiving all of his or her rights to the work worldwide under copyright law, including all related and neighboring rights, to the extent allowed by law. You can copy, modify, distribute and perform the work, even for commercial purposes, all without asking permission.
___Begin Copy/Paste Below Here_
Use this document to add these high-contrast themes with a background and a contrasting text color:
Sepia, Midnight, Terminal, Ruby, Sapphire, Emerald, Amethyst, Topaz, Obsidian, Alabaster, Coral, Slate.
Report: Implementing a Multi-Theme System in Next.js & Tailwind CSS
This report provides a comprehensive, step-by-step guide on how the dynamic theme-switching functionality, including standard and high-contrast themes, was implemented. This pattern is highly reusable for other Next.js applications using Tailwind CSS.
1. The Core Concept: CSS Variables
The entire system is built on a foundation of CSS variables defined in src/app/globals.css
. By centralizing color definitions here, we can make the entire application’s color scheme dynamic. Tailwind CSS is configured to use these variables for all its color utilities (e.g., bg-primary
, text-foreground
), which is the key to making this system work.
1.1. Base Theme Definitions (:root
and .dark
)
We define two primary color schemes: a default light mode (:root
) and a dark mode (.dark
). Every color used by the ShadCN UI components and our custom Tailwind classes is mapped to a CSS variable.
File: src/app/globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 96%; /* Light Mode Background */
--foreground: 240 10% 3.9%; /* Light Mode Text */
--card: 0 0% 100%;
--primary: 217 90% 60%; /* Vibrant Blue */
--accent: 16 100% 57%; /* Lively Orange */
/* ... and all other color variables */
}
.dark {
--background: 240 10% 3.9%; /* Dark Mode Background */
--foreground: 0 0% 98%; /* Dark Mode Text */
--card: 240 10% 3.9%;
--primary: 217 90% 60%;
--accent: 16 100% 57%;
/* ... and all other color variables for dark mode */
}
}
1.2. Custom High-Contrast Themes
Each custom theme is simply a new CSS class that overrides the same set of CSS variables with different HSL (Hue, Saturation, Lightness) values. When the next-themes
library adds a class like .sepia
to the <html>
tag, the browser instantly uses the variable definitions within that class scope, re-coloring the entire application without a page reload.
File: src/app/globals.css
/* ... after the :root and .dark definitions ... */
/* ... after the :root and .dark definitions ... */
.sepia {
--background: 38 56% 94%; --foreground: 31 15% 26%;
--card: 38 56% 94%; --card-foreground: 31 15% 26%;
--popover: 38 56% 94%; --popover-foreground: 31 15% 26%;
--primary: 31 15% 26%; --primary-foreground: 38 56% 94%;
--secondary: 31 15% 85%; --secondary-foreground: 31 15% 26%;
--muted: 31 15% 85%; --muted-foreground: 31 15% 46%;
--accent: 31 15% 85%; --accent-foreground: 31 15% 26%;
--destructive: 0 84% 60%; --destructive-foreground: 38 56% 94%;
--border: 31 15% 75%; --input: 31 15% 75%; --ring: 31 15% 26%;
}
.midnight {
--background: 216 28% 7%; --foreground: 210 15% 85%;
--card: 216 28% 7%; --card-foreground: 210 15% 85%;
--popover: 216 28% 7%; --popover-foreground: 210 15% 85%;
--primary: 210 15% 85%; --primary-foreground: 216 28% 7%;
--secondary: 216 28% 17%; --secondary-foreground: 210 15% 85%;
--muted: 216 28% 17%; --muted-foreground: 210 15% 65%;
--accent: 216 28% 17%; --accent-foreground: 210 15% 85%;
--destructive: 0 62% 30%; --destructive-foreground: 210 15% 85%;
--border: 216 28% 27%; --input: 216 28% 27%; --ring: 210 15% 85%;
}
.terminal {
--background: 0 0% 0%; --foreground: 120 100% 50%;
--card: 0 0% 0%; --card-foreground: 120 100% 50%;
--popover: 0 0% 0%; --popover-foreground: 120 100% 50%;
--primary: 120 100% 50%; --primary-foreground: 0 0% 0%;
--secondary: 0 0% 10%; --secondary-foreground: 120 100% 50%;
--muted: 0 0% 10%; --muted-foreground: 120 100% 30%;
--accent: 0 0% 10%; --accent-foreground: 120 100% 50%;
--destructive: 0 100% 50%; --destructive-foreground: 0 0% 0%;
--border: 120 100% 20%; --input: 120 100% 20%; --ring: 120 100% 50%;
}
.ruby {
--background: 350 60% 10%; --foreground: 350 80% 90%;
--card: 350 60% 10%; --card-foreground: 350 80% 90%;
--popover: 350 60% 10%; --popover-foreground: 350 80% 90%;
--primary: 350 80% 90%; --primary-foreground: 350 60% 10%;
--secondary: 350 60% 20%; --secondary-foreground: 350 80% 90%;
--muted: 350 60% 20%; --muted-foreground: 350 80% 70%;
--accent: 350 60% 20%; --accent-foreground: 350 80% 90%;
--destructive: 0 84% 60%; --destructive-foreground: 350 60% 10%;
--border: 350 60% 30%; --input: 350 60% 30%; --ring: 350 80% 90%;
}
.sapphire {
--background: 210 60% 10%; --foreground: 210 80% 90%;
--card: 210 60% 10%; --card-foreground: 210 80% 90%;
--popover: 210 60% 10%; --popover-foreground: 210 80% 90%;
--primary: 210 80% 90%; --primary-foreground: 210 60% 10%;
--secondary: 210 60% 20%; --secondary-foreground: 210 80% 90%;
--muted: 210 60% 20%; --muted-foreground: 210 80% 70%;
--accent: 210 60% 20%; --accent-foreground: 210 80% 90%;
--destructive: 0 84% 60%; --destructive-foreground: 210 60% 10%;
--border: 210 60% 30%; --input: 210 60% 30%; --ring: 210 80% 90%;
}
.emerald {
--background: 145 60% 10%; --foreground: 145 80% 90%;
--card: 145 60% 10%; --card-foreground: 145 80% 90%;
--popover: 145 60% 10%; --popover-foreground: 145 80% 90%;
--primary: 145 80% 90%; --primary-foreground: 145 60% 10%;
--secondary: 145 60% 20%; --secondary-foreground: 145 80% 90%;
--muted: 145 60% 20%; --muted-foreground: 145 80% 70%;
--accent: 145 60% 20%; --accent-foreground: 145 80% 90%;
--destructive: 0 84% 60%; --destructive-foreground: 145 60% 10%;
--border: 145 60% 30%; --input: 145 60% 30%; --ring: 145 80% 90%;
}
.amethyst {
--background: 270 60% 10%; --foreground: 270 80% 90%;
--card: 270 60% 10%; --card-foreground: 270 80% 90%;
--popover: 270 60% 10%; --popover-foreground: 270 80% 90%;
--primary: 270 80% 90%; --primary-foreground: 270 60% 10%;
--secondary: 270 60% 20%; --secondary-foreground: 270 80% 90%;
--muted: 270 60% 20%; --muted-foreground: 270 80% 70%;
--accent: 270 60% 20%; --accent-foreground: 270 80% 90%;
--destructive: 0 84% 60%; --destructive-foreground: 270 60% 10%;
--border: 270 60% 30%; --input: 270 60% 30%; --ring: 270 80% 90%;
}
.topaz {
--background: 45 60% 10%; --foreground: 45 80% 90%;
--card: 45 60% 10%; --card-foreground: 45 80% 90%;
--popover: 45 60% 10%; --popover-foreground: 45 80% 90%;
--primary: 45 80% 90%; --primary-foreground: 45 60% 10%;
--secondary: 45 60% 20%; --secondary-foreground: 45 80% 90%;
--muted: 45 60% 20%; --muted-foreground: 45 80% 70%;
--accent: 45 60% 20%; --accent-foreground: 45 80% 90%;
--destructive: 0 84% 60%; --destructive-foreground: 45 60% 10%;
--border: 45 60% 30%; --input: 45 60% 30%; --ring: 45 80% 90%;
}
.obsidian {
--background: 240 10% 4%; --foreground: 0 0% 100%;
--card: 240 10% 4%; --card-foreground: 0 0% 100%;
--popover: 240 10% 4%; --popover-foreground: 0 0% 100%;
--primary: 0 0% 100%; --primary-foreground: 240 10% 4%;
--secondary: 0 0% 14%; --secondary-foreground: 0 0% 100%;
--muted: 0 0% 14%; --muted-foreground: 0 0% 80%;
--accent: 0 0% 14%; --accent-foreground: 0 0% 100%;
--destructive: 0 84% 60%; --destructive-foreground: 240 10% 4%;
--border: 0 0% 24%; --input: 0 0% 24%; --ring: 0 0% 100%;
}
.alabaster {
--background: 0 0% 100%; --foreground: 240 10% 4%;
--card: 0 0% 100%; --card-foreground: 240 10% 4%;
--popover: 0 0% 100%; --popover-foreground: 240 10% 4%;
--primary: 240 10% 4%; --primary-foreground: 0 0% 100%;
--secondary: 0 0% 90%; --secondary-foreground: 240 10% 4%;
--muted: 0 0% 90%; --muted-foreground: 240 10% 24%;
--accent: 0 0% 90%; --accent-foreground: 240 10% 4%;
--destructive: 0 84% 60%; --destructive-foreground: 0 0% 100%;
--border: 0 0% 80%; --input: 0 0% 80%; --ring: 240 10% 4%;
}
.coral {
--background: 16 72% 95%; --foreground: 5 46% 22%;
--card: 16 72% 95%; --card-foreground: 5 46% 22%;
--popover: 16 72% 95%; --popover-foreground: 5 46% 22%;
--primary: 5 46% 22%; --primary-foreground: 16 72% 95%;
--secondary: 16 72% 85%; --secondary-foreground: 5 46% 22%;
--muted: 16 72% 85%; --muted-foreground: 5 46% 42%;
--accent: 16 72% 85%; --accent-foreground: 5 46% 22%;
--destructive: 0 84% 60%; --destructive-foreground: 16 72% 95%;
--border: 16 72% 75%; --input: 16 72% 75%; --ring: 5 46% 22%;
}
.slate {
--background: 220 14% 15%; --foreground: 210 40% 98%;
--card: 220 14% 15%; --card-foreground: 210 40% 98%;
--popover: 220 14% 15%; --popover-foreground: 210 40% 98%;
--primary: 210 40% 98%; --primary-foreground: 220 14% 15%;
--secondary: 220 14% 25%; --secondary-foreground: 210 40% 98%;
--muted: 220 14% 25%; --muted-foreground: 210 40% 78%;
--accent: 220 14% 25%; --accent-foreground: 210 40% 98%;
--destructive: 0 62% 30%; --destructive-foreground: 210 40% 98%;
--border: 220 14% 35%; --input: 220 14% 35%; --ring: 210 40% 98%;
}```
---
## **2. Tailwind CSS Configuration**
The `tailwind.config.ts` file is configured to *consume* these CSS variables instead of using hard-coded color values. This is the crucial link that makes Tailwind's utility classes aware of our dynamic themes.
The `colors` section of the configuration references the variables from `globals.css` using the `hsl(var(...))` syntax.
**File:** `tailwind.config.ts`
```typescript
import type {Config} from 'tailwindcss';
export default {
darkMode: ['class'],
content: [
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
// ...
],
theme: {
extend: {
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))',
},
// ... and so on for every color variable
},
},
},
plugins: [require('tailwindcss-animate')],
} satisfies Config;
3. Theme Management with next-themes
The next-themes
library is the engine that manages theme switching. It handles applying the correct class to the <html>
element and, importantly, persists the user’s choice in their browser’s local storage.
3.1. ThemeProvider
Setup
The entire application in src/app/layout.tsx
is wrapped with a ThemeProvider
component. This component provides the necessary React Context for the useTheme
hook to function anywhere in the app.
File: src/app/layout.tsx
import { ThemeProvider } from '@/components/layout/theme-provider';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider
attribute="class" // This tells it to change themes by adding a class to the <html> tag
defaultTheme="system"
enableSystem // Allows theme to sync with user's OS preference
disableTransitionOnChange
>
{/* The rest of the application */}
{children}
</ThemeProvider>
</body>
</html>
);
}
3.2. ThemeProvider
Component
The actual ThemeProvider
is a simple client component that re-exports the provider from next-themes
.
File: src/components/layout/theme-provider.tsx
"use client"
import * as React from "react"
import { ThemeProvider as NextThemesProvider } from "next-themes"
import type { ThemeProviderProps } from "next-themes/dist/types"
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}
4. The User Interface for Switching Themes
The final piece is the UI that allows the user to select a theme. This is implemented on the settings page.
File: src/app/(app)/settings/page.tsx
This client component uses the useTheme
hook from next-themes
to get the current theme and the setTheme
function to change it.
"use client"
import { useTheme } from "next-themes"
import { Button } from "@/components/ui/button";
import { Palette } from "lucide-react";
export default function SettingsPage() {
const { setTheme, theme } = useTheme()
const handleThemeChange = (newTheme: string) => {
setTheme(newTheme);
// A full page refresh is a direct way to ensure all styles are
// reapplied correctly after a theme change.
setTimeout(() => {
window.location.reload();
}, 100);
};
return (
<>
{/* ... Page Header ... */}
<Card>
{/* ... Card Header ... */}
<CardContent>
{/* ... Standard Themes ... */}
<div className="space-y-2">
<h3 className="text-lg font-medium">High-Contrast Themes</h3>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2 pt-2">
<Button
variant={theme === "sepia" ? "default" : "outline"}
onClick={() => handleThemeChange("sepia")}
>
<Palette className="mr-2 h-4 w-4" />
Sepia
</Button>
<Button
variant={theme === "midnight" ? "default" : "outline"}
onClick={() => handleThemeChange("midnight")}
>
<Palette className="mr-2 h-4 w-4" />
Midnight
</Button>
{/* ... Buttons for all other themes ... */}
</div>
</div>
</CardContent>
</Card>
</>
)
}
Summary of the Workflow
- User Action: The user clicks a theme button on the
/settings
page. next-themes
: ThesetTheme('theme-name')
function is called.- DOM Manipulation: The library updates the
<html>
tag by adding or changing itsclass
attribute (e.g.,<html class="sepia">
). - CSS Cascade: The browser’s CSS engine recognizes the new class and applies the corresponding CSS variable overrides defined in
globals.css
. - Tailwind Integration: Because all Tailwind utilities are mapped to these CSS variables, every component in the application that uses a theme color (e.g.,
bg-primary
,text-foreground
) instantly updates to reflect the new theme values.