Implementing dark mode in your Next.js application sounds simple, right? You just add a toggle button and some CSS classes, and voila—dark mode. But if you’ve tried this recently, you’ve probably run into a tiny, yet annoying problem: your carefully crafted toggle still stubbornly follows the user’s browser theme. You’re clicking your toggle button, but at times it feels like the browser’s built-in dark or light setting has the final say.
Let’s clear things up. Chances are, your current setup involves a reactive theme store, likely using Zustand or Context, and Tailwind CSS’s default integration for dark mode. Let’s take a quick look at how this typically works in your Next.js project.
Understanding the Current Implementation
In most Next.js projects, you store your theme state in a global store like Zustand or React’s Context API. Here’s a common snippet you might recognize:
// themeStore.ts
import { create } from 'zustand';
export const useThemeStore = create((set) => ({
theme: 'light',
setTheme: (theme) => set({ theme }),
}));
This is straightforward—you store a theme value (“light” or “dark”) and set it based on user input. However, Tailwind CSS itself typically checks your user’s operating system or browser settings by default. Usually, it checks the prefers-color-scheme media query to choose between “dark” or “light” mode.
Here’s what your tailwind.config.ts probably looks like:
// tailwind.config.ts
export default {
darkMode: 'media', // Automatically matches the user's OS/browser theme
content: ['./src/**/*.{ts,tsx}'],
theme: {
extend: {},
},
plugins: [],
};
Notice the issue here—this configuration explicitly ties your theme to ‘media’, meaning the browser’s preference reigns supreme and can override your toggle button.
The toggle button you typically implement likely looks something like this:
// ThemeToggleButton.tsx
import { useThemeStore } from '@/stores/themeStore';
const ThemeToggleButton = () => {
const { theme, setTheme } = useThemeStore();
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle Theme
</button>
);
};
export default ThemeToggleButton;
This button simply switches the global theme state between “dark” and “light.” But here’s the catch: even when you select “dark,” Tailwind can override this selection if your browser or OS is set to “light.” This is way too confusing and interferes with great user experience.
The Problem with Browser Theme Dependency
Why exactly does this happen? Well, when your Tailwind configuration is set to ‘media’, it means dark mode toggling relies directly on the browser’s prefers-color-scheme media feature. Your custom toggle button and theme store essentially become secondary to the user’s browser preference.
This creates confusion. Imagine you’re using YouTube or Twitter; when you select dark mode from the settings, you expect the page to stay dark no matter your system settings. But with the current setup, your Next.js app isn’t behaving this way—it stubbornly follows the browser or OS, spoiling the user experience.
Fixing the Browser Theme Dependency Issue
To solve this, we need to detach your Tailwind dark mode configuration from the browser’s theme. Instead of using ‘media’, we should leverage Tailwind’s ‘class’ mode. With ‘class’ mode, the presence of a specific class on your document element decides if dark mode is active.
Here’s what you need to do—just modify your tailwind.config.ts file as shown below:
// fixed tailwind.config.ts
export default {
darkMode: 'class', // Updated to use class-based dark mode
content: ['./src/**/*.{ts,tsx}'],
theme: {
extend: {},
},
};
With this change, your dark mode depends purely on whether the “dark” class is present on your document root. You now have complete control.
Next, let’s adjust your theme provider or theme hook to actually toggle the “dark” class on the HTML body independently.
Updating the Toggle Button Functionality
Your earlier simple toggle button no longer suffices—now we need the button to add or remove the “dark” class from your HTML root element explicitly. Let’s update it clearly:
// Improved ThemeToggleButton.tsx
import { useEffect } from 'react';
import { useThemeStore } from '@/stores/themeStore';
const ThemeToggleButton = () => {
const { theme, setTheme } = useThemeStore();
useEffect(() => {
if (theme === 'dark') {
document.documentElement.classList.add('dark');
localStorage.setItem('theme', 'dark');
} else {
document.documentElement.classList.remove('dark');
localStorage.setItem('theme', 'light');
}
}, [theme]);
const toggleTheme = () => {
setTheme(theme === 'dark' ? 'light' : 'dark');
};
return (
<button onClick={toggleTheme}>
Switch to {theme === 'dark' ? 'Light Mode' : 'Dark Mode'}
</button>
);
};
export default ThemeToggleButton;
In this improved component:
- You explicitly toggle the ‘dark’ class on your document.documentElement.
- You persist user’s choice in local storage, keeping it consistent on page reloads.
Testing and Validating Your Dark Mode Implementation
Before you deploy, it’s essential to validate your changes thoroughly. Here are some clear-cut ways of ensuring the new implementation works:
- Manual Testing: Simply toggle the button back and forth. Refresh the page to ensure persistence.
- Browser DevTools: Open DevTools in Chrome or Firefox to confirm the “dark” class is correctly applied to your HTML element.
- Cross-browser compatibility: Test your app across multiple browsers, like Chrome, Firefox, Edge, and Safari. Use BrowserStack if needed.
- Check Local Storage: Open your browser’s local storage in the Application tab (Chrome/Edge), confirming the theme persistence there.
- Incognito Mode: Ensure the default theme is applied during first load with no saved preferences.
These simple tests ensure your Next.js application behaves predictably across different user scenarios and browsers, preventing unexpected theme mode shifts.
Moving away from browser-dependent dark mode implementations isn’t just about aesthetics—it’s a crucial UX improvement. Users expect control when selecting preferences on a website. Your Next.js app should always respect this user expectation, despite browser or OS themes.
By updating your Tailwind CSS strategy and cleverly toggling the HTML root classes yourself, you’ve effectively taken full control of your application’s dark mode behavior. The result? A seamless experience that your users can trust—and genuinely appreciate.
Independent theme handling is truly best practice in modern web applications. Storing user selections in local storage not only enhances usability—it makes your site feel reliable and professional.
Looking for more JavaScript and Next.js tips? Feel free to explore additional articles at our JavaScript articles library, and let us know—what UX issue did you recently tackle on your Next.js project?
0 Comments