JavaScript 5 min read

Implementing Dark Mode with React Hooks

Learn how to add dark mode functionality to your React application using useState and useEffect hooks with localStorage persistence.

MR

Moshiour Rahman

· Updated May 9, 2026

Overview

Dark mode has become a must-have feature for modern websites and applications. Industry leaders like Twitter, Reddit, and YouTube all support dark mode. It’s more than just a trend - it’s easier on the eyes and perfect for nighttime use.

In this tutorial, you’ll learn how to implement dark mode in a React application using hooks. This is also a practical example of how to use useState and useEffect effectively.

Project Setup

Create a new React application using Create React App:

npx create-react-app react-darkmode-app
cd react-darkmode-app

Note: Make sure you have Node.js installed. Download it from nodejs.org if needed.

Setting Up the Theme Styles

First, let’s create our CSS with light and dark theme classes:

/* src/App.css */

body {
  margin: 0;
  text-align: center;
  transition: background-color 0.3s ease, color 0.3s ease;
}

/* Light Theme */
.light-theme {
  background-color: #ffffff;
  color: #1a1a2e;
}

.light-theme nav {
  background-color: #6366f1;
}

/* Dark Theme */
.dark-theme {
  background-color: #1a1a2e;
  color: #f1f1f1;
}

.dark-theme nav {
  background-color: #16213e;
}

.dark-theme code {
  color: #f472b6;
}

/* Common Styles */
nav {
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 1rem 2rem;
  color: white;
  transition: background-color 0.3s ease;
}

.content {
  padding: 2rem;
  margin: 0 auto;
  max-width: 600px;
  min-height: 80vh;
}

.theme-toggle {
  padding: 0.75rem 1.5rem;
  font-size: 1rem;
  font-weight: 600;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  background-color: rgba(255, 255, 255, 0.2);
  color: white;
  transition: all 0.2s ease;
}

.theme-toggle:hover {
  background-color: rgba(255, 255, 255, 0.3);
  transform: translateY(-2px);
}

Persisting User Preference

We want to save the user’s theme preference so it persists across sessions. We’ll use localStorage for this:

// Get the initial theme from localStorage or default to light
const getInitialTheme = () => {
  const savedTheme = localStorage.getItem('theme');
  return savedTheme ? savedTheme === 'dark' : false;
};

The Complete App Component

Here’s the full implementation:

// src/App.jsx
import React, { useState, useEffect } from 'react';
import './App.css';

function App() {
  // Initialize state from localStorage
  const getInitialTheme = () => {
    const savedTheme = localStorage.getItem('theme');
    if (savedTheme) {
      return savedTheme === 'dark';
    }
    // Check system preference as fallback
    return window.matchMedia('(prefers-color-scheme: dark)').matches;
  };

  const [isDarkMode, setIsDarkMode] = useState(getInitialTheme);

  // Persist theme preference to localStorage
  useEffect(() => {
    localStorage.setItem('theme', isDarkMode ? 'dark' : 'light');
  }, [isDarkMode]);

  // Toggle function
  const toggleTheme = () => {
    setIsDarkMode(prevMode => !prevMode);
  };

  return (
    <div className={isDarkMode ? 'dark-theme' : 'light-theme'}>
      <nav>
        <button className="theme-toggle" onClick={toggleTheme}>
          {isDarkMode ? '☀️ Light Mode' : '🌙 Dark Mode'}
        </button>
      </nav>

      <div className="content">
        <h1>{isDarkMode ? '🌙 Dark Mode' : '☀️ Light Mode'}</h1>
        <p>
          Click the button above to toggle between light and dark themes.
          Your preference will be saved automatically!
        </p>
        <p>
          Try refreshing the page - your theme choice will persist.
        </p>
      </div>
    </div>
  );
}

export default App;

How It Works

Let’s break down the key parts:

1. State Initialization

const [isDarkMode, setIsDarkMode] = useState(getInitialTheme);

The getInitialTheme function checks:

  1. First, if there’s a saved preference in localStorage
  2. Falls back to the system’s color scheme preference

2. Persisting Changes with useEffect

useEffect(() => {
  localStorage.setItem('theme', isDarkMode ? 'dark' : 'light');
}, [isDarkMode]);

useEffect runs whenever isDarkMode changes, saving the new preference to localStorage.

3. Toggle Function

const toggleTheme = () => {
  setIsDarkMode(prevMode => !prevMode);
};

We use the callback form of setState to toggle based on the previous value. This is safer than directly using !isDarkMode.

4. Conditional Styling

<div className={isDarkMode ? 'dark-theme' : 'light-theme'}>

The theme class is applied based on the current state, triggering our CSS transitions.

Enhanced Version with Custom Hook

For better reusability, extract the logic into a custom hook:

// src/hooks/useTheme.js
import { useState, useEffect } from 'react';

export function useTheme() {
  const getInitialTheme = () => {
    if (typeof window === 'undefined') return false;

    const savedTheme = localStorage.getItem('theme');
    if (savedTheme) {
      return savedTheme === 'dark';
    }

    return window.matchMedia('(prefers-color-scheme: dark)').matches;
  };

  const [isDarkMode, setIsDarkMode] = useState(getInitialTheme);

  useEffect(() => {
    localStorage.setItem('theme', isDarkMode ? 'dark' : 'light');

    // Optional: Update the document class for global styling
    document.documentElement.classList.toggle('dark', isDarkMode);
  }, [isDarkMode]);

  const toggleTheme = () => setIsDarkMode(prev => !prev);

  return { isDarkMode, toggleTheme };
}

Usage:

import { useTheme } from './hooks/useTheme';

function App() {
  const { isDarkMode, toggleTheme } = useTheme();

  return (
    <div className={isDarkMode ? 'dark-theme' : 'light-theme'}>
      <button onClick={toggleTheme}>Toggle Theme</button>
    </div>
  );
}

Conclusion

You’ve learned how to:

  • Implement dark mode using React hooks
  • Persist user preferences with localStorage
  • Use useState for state management
  • Use useEffect for side effects
  • Create a reusable custom hook

Source Code: GitHub Repository

Key Takeaways:

  • Always provide a fallback for first-time users (system preference)
  • Use smooth CSS transitions for a polished experience
  • Extract reusable logic into custom hooks
MR

Moshiour Rahman

Software Architect & AI Engineer

Share:
MR

Moshiour Rahman

Software Architect & AI Engineer

Enterprise software architect with deep expertise in financial systems, distributed architecture, and AI-powered applications. Building large-scale systems at Fortune 500 companies. Specializing in LLM orchestration, multi-agent systems, and cloud-native solutions. I share battle-tested patterns from real enterprise projects.

Related Articles

Comments

Comments are powered by GitHub Discussions.

Configure Giscus at giscus.app to enable comments.