This content originally appeared on DEV Community and was authored by Rhythm Saha
Hey everyone, Rhythm Saha here! I’m the founder of NovexiQ, and we’re all about crafting modern web applications that truly stand out. As a fullstack developer deeply entrenched in the MERN stack, Next.js, and Tailwind CSS, I’m constantly exploring ways to elevate user experience and deliver those polished, professional applications we all love. And you know what’s a feature users absolutely adore and pretty much expect these days? You guessed it – dark mode!
Dark mode isn’t just some fleeting trend; it’s a genuinely critical accessibility feature. Plus, it’s a huge personal preference for so many users, especially for those long coding sessions or when you’re working in low-light environments. It’s great for reducing eye strain, it can even save battery on OLED screens, and let’s be honest, it just looks super cool on modern interfaces. If you’re building with React or Next.js and styling with Tailwind CSS, getting a robust dark mode toggle up and running is actually way more straightforward than you might imagine.
In this comprehensive guide, I’m going to walk you through the entire process, step-by-step, of integrating dark mode into your React or Next.js application using Tailwind CSS. We’ll cover everything: from configuring Tailwind properly, to setting up a global theme context, persisting your users’ preferences, and finally, building a sleek toggle component. By the time we’re done, you’ll have a professional-grade dark mode implementation ready for your next big project, exactly the kind we love building here at NovexiQ.
Prerequisites
Before we dive in, let’s make sure you’ve got a few things set up and ready to go:
- A new or existing Next.js or React project.
- Tailwind CSS installed and configured in your project. If you haven’t done this, check out the official Tailwind CSS documentation for installation guides.
- Basic understanding of React hooks (
useState
,useEffect
,useContext
) and TypeScript (though the concepts apply to JavaScript too).
Step 1: Configure Tailwind CSS for Dark Mode
Tailwind CSS makes implementing dark mode incredibly simple, thanks to its darkMode
configuration. We’re going to set it to 'class'
. What this means is that Tailwind will magically apply dark mode styles whenever the dark
class is present higher up in your HTML tree – usually right on the html
element itself.
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: 'class', // This is the key change!
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {},
},
plugins: [],
};
So, by setting darkMode: 'class'
, Tailwind CSS gets ready to generate those awesome dark:
variants for all its utility classes. What does that mean exactly? Well, a class like dark:bg-gray-800
will *only* apply that dark gray background when the dark
class is present higher up in your HTML tree, typically on the <html>
element itself. Pretty neat, right?
Step 2: Create a Theme Context and Provider
To manage your theme state (light or dark) globally across your application and, super importantly, to persist it, we’re going to leverage React’s Context API. This approach makes it incredibly easy for any component to access and update the current theme.
// src/context/ThemeContext.tsx
import React, { createContext, useState, useEffect, useContext, ReactNode } from 'react';
// Define the shape of our context
interface ThemeContextType {
theme: string;
toggleTheme: () => void;
}
// Create the context with a default undefined value (or a dummy default)
export const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
// Props for the ThemeProvider component
interface ThemeProviderProps {
children: ReactNode;
}
export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
// Initialize theme from localStorage or user preference
const [theme, setTheme] = useState<string>(() => {
if (typeof window !== 'undefined') {
const storedTheme = localStorage.getItem('theme');
if (storedTheme) return storedTheme;
// Check for user's system preference
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
return 'light'; // Default for server-side rendering
});
// Effect to apply/remove 'dark' class on the html element
useEffect(() => {
const root = window.document.documentElement;
if (theme === 'dark') {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}
// Store the theme in localStorage
localStorage.setItem('theme', theme);
}, [theme]); // Re-run effect when theme changes
// Function to toggle between themes
const toggleTheme = () => {
setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light'));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
// Custom hook to use the theme context easily
export const useTheme = () => {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
};
Alright, let’s break down what’s happening in this file:
-
ThemeContext
: This is where we create the actual context itself. -
ThemeProvider
: This crucial component will wrap our entire application (or whichever part you want to apply the theme to).- First off, it uses
useState
to manage the currenttheme
. - The initial theme setup is pretty smart! It first checks your
localStorage
to see if a theme was already saved from a previous visit. If not, it gracefully falls back to checking your user’s system preference usingwindow.matchMedia('(prefers-color-scheme: dark)')
. This is awesome because it ensures users get a consistent experience right from their very first load, respecting their system settings. - Now,
useEffect
is super, super crucial here! Whenever thetheme
state changes, this effect kicks right in:- It either adds or removes the
dark
class from the<html>
element – remember, that’s the magic Tailwind CSS uses to apply dark mode styles. - It also persists the chosen theme in
localStorage
. So, your user’s preference is remembered even if they close and reopen their browser. No more annoying theme resets!
- It either adds or removes the
- Finally, the
toggleTheme
function simply flips the theme from ‘light’ to ‘dark’ and vice-versa. See? Pretty straightforward, right?
- First off, it uses
-
useTheme
: This is a neat custom hook that makes consuming our theme context super easy in any component.
Step 3: Integrate the ThemeProvider into Your Application
For Next.js applications, you’ll typically want to wrap your entire application within pages/_app.tsx
. If you’re working with a standard React app, you’d usually place this in your index.tsx\
or App.tsx\
file.
// pages/_app.tsx (for Next.js)
import type { AppProps } from 'next/app';
import { ThemeProvider } from '../src/context/ThemeContext'; // Adjust path as needed
import '../styles/globals.css'; // Your global styles including Tailwind imports
function MyApp({ Component, pageProps }: AppProps) {
return (
<ThemeProvider>
<Component {...pageProps} />
</ThemeProvider>
);
}
export default MyApp;
And just like that, every single component within your application can now effortlessly access our theme context! How cool is that?
Step 4: Create a Theme Toggle Component
Alright, now for the fun part! Let’s build the actual UI element that your users will interact with to switch themes. This component will make good use of our useTheme
hook.
// src/components/ThemeToggle.tsx
import React from 'react';
import { useTheme } from '../context/ThemeContext'; // Adjust path as needed
// You might use an icon library like Heroicons for better icons
// For simplicity, we'll just use text or simple SVGs here.
// Example SVG for sun and moon:
const SunIcon = () => (
<svg className="w-6 h-6 text-yellow-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 3v1m0 16v1m9-9h1M3 12H2m15.325-4.675l.707-.707M6.071 18.071l-.707.707M17.325 17.325l.707.707M6.071 6.071l-.707-.707"></path></svg>
);
const MoonIcon = () => (
<svg className="w-6 h-6 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"></path></svg>
);
const ThemeToggle: React.FC = () => {
const { theme, toggleTheme } = useTheme();
return (
<button
onClick={toggleTheme}
className="p-2 rounded-full focus:outline-none focus:ring-2 focus:ring-indigo-500 transition-colors duration-300"
aria-label="Toggle dark mode"
>
{theme === 'light' ? <SunIcon /> : <MoonIcon />}
</button>
);
};
export default ThemeToggle;
You can now place this ThemeToggle
component anywhere you like in your application – it’s typically found in a header or a navigation bar, making it easily accessible. Time to venture to the dark side… or the light!
Step 5: Apply Dark Mode Styles with Tailwind CSS
With the dark
class dynamically being added or removed from your html\
element, applying dark mode styles becomes unbelievably simple. It’s just a matter of using Tailwind’s awesome dark:
prefix!
// src/components/Layout.tsx
import React, { ReactNode } from 'react';
import ThemeToggle from './ThemeToggle'; // Adjust path as needed
interface LayoutProps {
children: ReactNode;
}
const Layout: React.FC<LayoutProps> = ({ children }) => {
return (
<div className="min-h-screen bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 transition-colors duration-300">
<header className="p-4 flex justify-between items-center bg-gray-100 dark:bg-gray-800 shadow-sm transition-colors duration-300">
<h1 className="text-2xl font-bold text-gray-800 dark:text-gray-200">NovexiQ Blog</h1>
<ThemeToggle />
</header>
<main className="p-4 max-w-4xl mx-auto">
{children}
</main>
</div>
);
};
export default Layout;
Notice how we’re using classes like dark:bg-gray-900
and dark:text-gray-100
? Here’s the magic: when the <html>
element gets that dark
class, these specific styles will gracefully override your default light mode styles. And hey, don’t forget that transition-colors duration-300
class! It adds a super smooth animation when switching themes, making the whole user experience much more pleasant.
You can apply this principle to any component:
- Buttons:
bg-indigo-500 dark:bg-indigo-600 text-white
- Cards:
bg-white dark:bg-gray-700 shadow dark:shadow-lg rounded-lg p-6
- Borders:
border border-gray-200 dark:border-gray-600
Refinement and Best Practices
1. Smooth Transitions
Always, *always* add transition-colors duration-300
(or something similar) to elements that change their color or background. Trust me on this one; it truly creates a much smoother and less jarring experience when the theme switches. Your users will absolutely appreciate it!
2. Initial User Preference
Good news! Our ThemeProvider
is already designed to handle this beautifully. It first checks your localStorage
, and then cleverly looks at window.matchMedia('(prefers-color-scheme: dark)')
. This is genuinely awesome because it respects your user’s system-wide preference right out of the box, ensuring a cohesive and thoughtful feel for them.
3. Accessibility Considerations
When you’re designing your dark mode, please, please pay extra attention to contrast. It’s super, super important to make sure that text and interactive elements have sufficient contrast ratios against their backgrounds in *both* light and dark modes. This isn’t just about aesthetics; it helps you meet WCAG guidelines and makes your app truly accessible to everyone. Tools like WebAIM Contrast Checker are your absolute best friends here!
4. Iconography
For your theme toggle, always use clear and intuitive icons (like a sun for light mode and a moon for dark mode). Libraries like Heroicons or Font Awesome are excellent choices that I highly recommend.
Conclusion
So there you have it! Adding dark mode to your React or Next.js application with Tailwind CSS is a fantastic way to really improve the user experience, offer awesome personalization, and proudly showcase a modern design. By cleverly leveraging Tailwind’s darkMode: 'class'
and React’s Context API, we’ve managed to build a robust, persistent, and incredibly easy-to-manage theme system. You’ve seen the light… or the dark!
Here at NovexiQ, we truly believe in building applications that aren’t just functional, but also genuinely delightful to use. And features like dark mode? They’re a perfect testament to that commitment. I genuinely hope this guide empowers you to integrate this incredibly popular feature into your projects with confidence.
If you have any questions at all, run into any issues, or just want to share your awesome dark mode implementations, please don’t hesitate to drop a comment below! I’d absolutely love to hear from you and see what you’re building. Happy coding, everyone!
— Rhythm Saha, Founder of NovexiQ
This content originally appeared on DEV Community and was authored by Rhythm Saha