In this blog post, we will explore how to implement multi-theme support in a Next.js application using CSS variables and the next-themes
package. This will allow users to switch between different themes seamlessly.
Prerequisites
Before we begin, ensure you have the following:
- A Next.js application set up.
- shadcn UI,
- The
next-themes
package installed. If you haven't installed it yet, run:
npm install next-themes
Step 1: Set Up Global CSS
Create a global CSS file to define your themes using CSS variables. This file will contain the styles for each theme.
File: app/global.css
css1@tailwind base; 2@tailwind components; 3@tailwind utilities; 4 5@layer base { 6 :root { 7 --background: 0 0% 100%; 8 --foreground: 240 10% 3.9%; 9 --card: 0 0% 100%; 10 --card-foreground: 240 10% 3.9%; 11 --popover: 0 0% 100%; 12 --popover-foreground: 240 10% 3.9%; 13 --primary: 240 5.9% 10%; 14 --primary-foreground: 0 0% 98%; 15 --secondary: 240 4.8% 95.9%; 16 --secondary-foreground: 240 5.9% 10%; 17 --muted: 240 4.8% 95.9%; 18 --muted-foreground: 240 3.8% 46.1%; 19 --accent: 240 4.8% 95.9%; 20 --accent-foreground: 240 5.9% 10%; 21 --destructive: 0 72.22% 50.59%; 22 --destructive-foreground: 0 0% 98%; 23 --border: 240 5.9% 90%; 24 --input: 240 5.9% 90%; 25 --ring: 240 5% 64.9%; 26 --radius: 0.5rem; 27 28 --chart-1: 12 76% 61%; 29 --chart-2: 173 58% 39%; 30 --chart-3: 197 37% 24%; 31 --chart-4: 43 74% 66%; 32 --chart-5: 27 87% 67%; 33 } 34 35 html[data-theme='shadcn-classic'] { 36 --background: 0 0% 100%; 37 --foreground: 240 10% 3.9%; 38 --card: 0 0% 100%; 39 --card-foreground: 240 10% 3.9%; 40 --popover: 0 0% 100%; 41 --popover-foreground: 240 10% 3.9%; 42 --primary: 240 5.9% 10%; 43 --primary-foreground: 0 0% 98%; 44 --secondary: 240 4.8% 95.9%; 45 --secondary-foreground: 240 5.9% 10%; 46 --muted: 240 4.8% 95.9%; 47 --muted-foreground: 240 3.8% 46.1%; 48 --accent: 240 4.8% 95.9%; 49 --accent-foreground: 240 5.9% 10%; 50 --destructive: 0 72.22% 50.59%; 51 --destructive-foreground: 0 0% 98%; 52 --border: 240 5.9% 90%; 53 --input: 240 5.9% 90%; 54 --ring: 240 5% 64.9%; 55 --radius: 0.5rem; 56 57 --chart-1: 12 76% 61%; 58 --chart-2: 173 58% 39%; 59 --chart-3: 197 37% 24%; 60 --chart-4: 43 74% 66%; 61 --chart-5: 27 87% 67%; 62 } 63 64 html[data-theme='shadcn-dark'] { 65 --background: 240 10% 3.9%; 66 --foreground: 0 0% 98%; 67 --card: 240 10% 3.9%; 68 --card-foreground: 0 0% 98%; 69 --popover: 240 10% 3.9%; 70 --popover-foreground: 0 0% 98%; 71 --primary: 0 0% 98%; 72 --primary-foreground: 240 5.9% 10%; 73 --secondary: 240 3.7% 15.9%; 74 --secondary-foreground: 0 0% 98%; 75 --muted: 240 3.7% 15.9%; 76 --muted-foreground: 240 5% 64.9%; 77 --accent: 240 3.7% 15.9%; 78 --accent-foreground: 0 0% 98%; 79 --destructive: 0 62.8% 30.6%; 80 --destructive-foreground: 0 85.7% 97.3%; 81 --border: 240 3.7% 15.9%; 82 --input: 240 3.7% 15.9%; 83 --ring: 240 4.9% 83.9%; 84 85 --chart-1: 220 70% 50%; 86 --chart-2: 160 60% 45%; 87 --chart-3: 30 80% 55%; 88 --chart-4: 280 65% 60%; 89 --chart-5: 340 75% 55%; 90 } 91 92 html[data-theme='terra-theme'] { 93 --background: 30 20% 95%; 94 --foreground: 30 10% 15%; 95 --primary: 30 60% 45%; 96 --primary-foreground: 30 15% 95%; 97 --secondary: 20 40% 65%; 98 --secondary-foreground: 20 10% 20%; 99 --accent: 10 70% 55%; 100 --accent-foreground: 10 15% 95%; 101 --destructive: 0 70% 50%; 102 --destructive-foreground: 0 15% 95%; 103 --muted: 30 30% 85%; 104 --muted-foreground: 30 10% 25%; 105 --card: 30 15% 90%; 106 --card-foreground: 30 10% 20%; 107 --popover: 30 10% 100%; 108 --popover-foreground: 30 10% 10%; 109 --border: 30 30% 70%; 110 --input: 30 30% 70%; 111 --ring: 30 60% 40%; 112 --radius: 0.5rem; 113} 114} 115 116@layer base { 117 * { 118 @apply border-border; 119 } 120 121 body { 122 @apply bg-background text-foreground font-body; 123 } 124 125 h1, h2, h3, h4, h5, h6 { 126 @apply font-heading; 127 } 128}
Step 2: Create a Theme Provider
Next, create a Theme Provider component that will wrap your application and manage the theme state.
File: app/theme-provider.tsx
typescript1"use client"; 2 3import * as React from "react"; 4import { ThemeProvider as NextThemesProvider } from "next-themes"; 5 6type ThemeProviderProps = Parameters<typeof NextThemesProvider>[0]; 7 8export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 9 return <NextThemesProvider {...props}>{children}</NextThemesProvider>; 10}
Step 3: Create a Theme Selector Component
Create a component that allows users to select their preferred theme. This component will use radio buttons to display the available themes.
File: app/theme-toggles.tsx
typescript1"use client"; 2 3import { useTheme } from "next-themes"; 4import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; 5import { Label } from "@/components/ui/label"; 6import { useEffect, useState } from "react"; 7 8const themes = [ 9 { 10 name: "Default", 11 value: "default", 12 colors: [ 13 "hsl(240, 5.9%, 10%)", // --primary 14 "hsl(240, 4.8%, 95.9%)", // --secondary 15 "hsl(240, 4.8%, 95.9%)", // --accent 16 "hsl(240, 5.9%, 90%)", // --border 17 ], 18 }, 19 { 20 name: "Dark", 21 value: "shadcn-dark", 22 colors: [ 23 "hsl(0, 0%, 98%)", // --primary 24 "hsl(240, 3.7%, 15.9%)", // --secondary 25 "hsl(240, 3.7%, 15.9%)", // --accent 26 "hsl(240, 3.7%, 15.9%)", // --border 27 ], 28 }, 29 { 30 name: "Terra", 31 value: "terra-theme", 32 colors: [ 33 "hsl(30, 60%, 45%)", // --primary 34 "hsl(20, 40%, 65%)", // --secondary 35 "hsl(10, 70%, 55%)", // --accent 36 "hsl(30, 30%, 70%)", // --border 37 ], 38 }, 39]; 40 41export function ThemeSelector() { 42 const { theme, setTheme } = useTheme(); 43 const [mounted, setMounted] = useState(false); 44 45 useEffect(() => setMounted(true), []); 46 47 if (!mounted) return null; 48 49 return ( 50 <RadioGroup value={theme} onValueChange={setTheme} className="space-y-2"> 51 {themes.map((themeOption) => ( 52 <div key={themeOption.value} className="flex items-center space-x-2"> 53 <RadioGroupItem value={themeOption.value} id={themeOption.value} /> 54 <Label 55 htmlFor={themeOption.value} 56 className="flex items-center space-x-2" 57 > 58 <span>{themeOption.name}</span> 59 <div className="flex"> 60 {themeOption.colors.map((color, index) => ( 61 <div 62 key={index} 63 style={{ backgroundColor: color }} 64 className="w-6 h-6 border border-gray-300" 65 /> 66 ))} 67 </div> 68 </Label> 69 </div> 70 ))} 71 </RadioGroup> 72 ); 73}
Step 4: Create a Theme Popover Component
This component will display the theme selector in a popover when the user clicks on a settings button.
File: app/theme-popover.tsx
typescript1import { Button } from "@/components/ui/button"; 2import { 3 Popover, 4 PopoverContent, 5 PopoverTrigger, 6} from "@/components/ui/popover"; 7import { Settings } from "lucide-react"; 8import { ThemeSelector } from "./theme-toggles"; 9import { Label } from "@/components/ui/label"; 10 11export function ThemePopOver() { 12 return ( 13 <Popover> 14 <PopoverTrigger asChild> 15 <Button variant="ghost" size="icon" className="h-10 w-10 ml-4"> 16 <Settings className="h-6 w-6" /> 17 </Button> 18 </PopoverTrigger> 19 <PopoverContent className="w-96"> 20 <div className="space-y-2"> 21 <Label>Theme</Label> 22 <ThemeSelector /> 23 </div> 24 </PopoverContent> 25 </Popover> 26 ); 27}
Step 5: Set Up the Layout
Integrate the Theme Provider into your layout component to ensure that the theme context is available throughout your application.
File: app/layout.tsx
typescript1import { ThemeProvider } from "./theme-provider"; 2import "./global.css"; 3 4interface RootLayoutProps { 5 children: React.ReactNode; 6} 7 8export default function Layout({ children }: RootLayoutProps) { 9 return ( 10 <html lang="en"> 11 <body className="antialiased overflow-hidden"> 12 <ThemeProvider 13 defaultTheme="light" 14 enableColorScheme 15 themes={[ 16 "light", 17 "shadcn-classic", 18 "dark-shadcn-classic", 19 "terra-theme", 20 ]} 21 > 22 <main className="flex flex-col flex-1 max-7xl mx-auto"> 23 {children} 24 </main> 25 </ThemeProvider> 26 </body> 27 </html> 28 ); 29}
Step 6: Use the Theme Popover in Your Home Component
Finally, include the Theme Popover in your main component to allow users to switch themes.
File: app/page.tsx
typescript1import { ThemePopOver } from "./theme-popover"; 2 3export default function Home() { 4 return <ThemePopOver />; 5}
Conclusion
You have successfully implemented multi-theme support in your Next.js application. Users can now switch between different themes seamlessly. Customize the themes further by adjusting the CSS variables in your global CSS file. Happy coding!
Project Links
You can find the complete code for this project in the GitHub repository:
Deployed Version
The application is deployed and can be accessed at: