Light and Dark Mode in Tailwind CSS v4 with Next.js

DarkMode
LightMode
ThemeToggle
TailwindCSS
NextJS
DarkLightMode
ModernWebDesign
Learn how to implement a modern and responsive dark/light mode toggle in your Next.js 14 app using Tailwind CSS v4 — with and without localStorage. Fully explained with code and best practices.
Why Dark Mode?
Dark mode isn’t just a trend it improves user experience by reducing eye strain and saving battery on OLED devices. Tailwind CSS v4 makes it easier and more customizable than ever.
In this blog, you’ll learn two complete approaches to toggling between light and dark themes:
- Without using localStorage (just user system preference)
- With localStorage (persistent across sessions)
1. Setup Next.js App
1npx create-next-app@latest dark-light-mode-app🎨 2. Add Global Styles (CSS Variables for Themes)
Inside your app/globals.css, define your custom theme using CSS variables:
1@import "tailwindcss";
2
3@custom-variant dark (&:where(.dark, .dark *));
4
5@theme inline {
6 --color-bg: var(--bg);
7 --color-fg: var(--fg);
8}
9
10:root {
11 --bg: #ffffff;
12 --fg: #000000;
13}
14
15.dark {
16 --bg: #0a0a0a;
17 --fg: #ffffff;
18}
19
20body {
21 background: var(--color-bg);
22 color: var(--color-fg);
23 transition: background 0.3s, color 0.3s;
24}
253. Create a Layout (app/layout.tsx)
Ensure that theme styles are applied correctly on load.
1import "./globals.css";
2
3export default function RootLayout({
4 children,
5}: {
6 children: React.ReactNode;
7}) {
8 return (
9 <html lang="en" suppressHydrationWarning>
10 <body className="bg-background text-foreground transition-colors duration-300">
11 {children}
12 </body>
13 </html>
14 );
15}
16🌙 4. Create the Theme Toggle Button (Without localStorage)
Create app/components/ThemeToggle.tsx:
1'use client'
2
3import { useEffect, useState } from 'react'
4import { Sun, Moon } from 'lucide-react'
5
6export default function ThemeToggle() {
7 const [isDark, setIsDark] = useState(false)
8
9 useEffect(() => {
10 const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
11 document.documentElement.classList.toggle('dark', prefersDark)
12 setIsDark(prefersDark)
13 }, [])
14
15 const toggleTheme = () => {
16 const newDark = !isDark
17 document.documentElement.classList.toggle('dark', newDark)
18 setIsDark(newDark)
19 }
20
21 return (
22 <button
23 onClick={toggleTheme}
24 className="w-10 h-10 rounded-full flex items-center justify-center
25 bg-yellow-300 dark:bg-gray-800 transition-colors"
26 >
27 {isDark ? <Moon className="text-white w-5 h-5" /> : <Sun className="text-black w-5 h-5" />}
28 </button>
29 )
30}
31This version relies on the user’s system theme and toggles class without saving preference.
💾 5. (Optional) Theme Toggle with localStorage
To remember the theme between reloads, use this version of the toggle:
1'use client'
2
3import { useEffect, useState } from 'react'
4import { Sun, Moon } from 'lucide-react'
5
6export default function ThemeToggle() {
7 const [isDark, setIsDark] = useState(false)
8
9 useEffect(() => {
10 const stored = localStorage.getItem('theme')
11 const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
12 const shouldUseDark = stored === 'dark' || (!stored && prefersDark)
13
14 setIsDark(shouldUseDark)
15 document.documentElement.classList.toggle('dark', shouldUseDark)
16 }, [])
17
18 const toggleTheme = () => {
19 const newDark = !isDark
20 setIsDark(newDark)
21 document.documentElement.classList.toggle('dark', newDark)
22 localStorage.setItem('theme', newDark ? 'dark' : 'light')
23 }
24
25 return (
26 <button
27 onClick={toggleTheme}
28 className="w-10 h-10 flex items-center justify-center rounded-full bg-yellow-300 dark:bg-gray-700"
29 >
30 {isDark ? <Moon className="w-5 h-5 text-white" /> : <Sun className="w-5 h-5 text-black" />}
31 </button>
32 )
33}
34This version checks and stores the theme preference in localStorage.
🏡 6. Add It to Your Page (app/page.tsx)
1import ThemeToggle from "./components/ThemeToggle";
2
3export default function Home() {
4 return (
5 <div className="bg-background text-foreground min-h-screen p-6">
6 <ThemeToggle />
7 <h1 className="text-2xl font-bold mt-6">
8 Light/Dark Mode with Tailwind CSS v4
9 </h1>
10 </div>
11 );
12}
13Why Dark Mode Is a UX Decision, Not Just a Feature
Dark mode often gets treated as a visual upgrade, but in real-world applications, it directly affects usability, accessibility, and user satisfaction.
Many users actively avoid apps that don’t respect their preferred theme. When dark mode feels inconsistent or resets unexpectedly, it breaks trust — especially in productivity or content-heavy platforms.
That’s why implementing theme switching correctly matters more than it seems.
User Preference vs Developer Preference
One mistake developers make is forcing a theme based on personal taste. In practice:
- Some users prefer dark mode during night hours
- Others switch themes based on lighting conditions
- Accessibility users may rely on higher contrast
A good implementation respects user choice first, then system preference, and only then default styling.
📱 Real-World Use Cases Where Dark Mode Matters Most
Dark mode has a noticeable impact in applications like:
- Developer dashboards and admin panels
- Reading platforms and blogs
- E-commerce apps used late at night
- Mobile-first apps with long session times
In these cases, poor theme handling leads to eye fatigue and faster drop-offs.
Pro Tips
- Use prefers-color-scheme only on first load, then rely on localStorage (if used).
- Add a darkMode toggle in the app settings or nav menu for better UX.
- Test on real devices for system preference detection.

