DEV Community

syauqi fuadi
syauqi fuadi

Posted on

Add Dark Mode to Your React App in 15 Minutes

Hey there! Let's add a sleek dark mode to your React app. We'll keep it simple but make it look professional. This tutorial builds on our previous task manager app, but you can apply it to any React project.

Screenshot of our dark mode app

What We'll Build

  • A toggle button for dark/light mode
  • Smooth color transitions
  • Persistent theme preference
  • System theme detection

Setting Up

First, we'll update our tailwind.config.js:

/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  darkMode: 'class', // This is important!
  theme: {
    extend: {},
  },
  plugins: [],
}
Enter fullscreen mode Exit fullscreen mode

Creating the Theme Context

Create a new file src/context/ThemeContext.tsx:

import { createContext, useContext, useEffect, useState } from 'react'

type ThemeContextType = {
  isDarkMode: boolean
  toggleDarkMode: () => void
}

const ThemeContext = createContext<ThemeContextType | undefined>(undefined)

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [isDarkMode, setIsDarkMode] = useState(() => {
    // Check localStorage and system preference
    const savedTheme = localStorage.getItem('theme')
    if (savedTheme) {
      return savedTheme === 'dark'
    }
    return window.matchMedia('(prefers-color-scheme: dark)').matches
  })

  useEffect(() => {
    // Update document class and localStorage
    document.documentElement.classList.toggle('dark', isDarkMode)
    localStorage.setItem('theme', isDarkMode ? 'dark' : 'light')
  }, [isDarkMode])

  const toggleDarkMode = () => {
    setIsDarkMode(!isDarkMode)
  }

  return (
    <ThemeContext.Provider value={{ isDarkMode, toggleDarkMode }}>
      {children}
    </ThemeContext.Provider>
  )
}

// Custom hook for using the theme
export function useTheme() {
  const context = useContext(ThemeContext)
  if (context === undefined) {
    throw new Error('useTheme must be used within a ThemeProvider')
  }
  return context
}
Enter fullscreen mode Exit fullscreen mode

Update App Entry Point

In src/main.tsx:

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
import { ThemeProvider } from './context/ThemeContext'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <ThemeProvider>
      <App />
    </ThemeProvider>
  </StrictMode>
)
Enter fullscreen mode Exit fullscreen mode

Creating the Theme Toggle Button

Create a new component src/components/ThemeToggle.tsx:

import { useTheme } from '../context/ThemeContext'

export function ThemeToggle() {
  const { isDarkMode, toggleDarkMode } = useTheme()

  return (
    <button
      onClick={toggleDarkMode}
      className="fixed top-4 right-4 p-2 rounded-lg bg-gray-200 dark:bg-gray-700 
                 transition-colors duration-200"
    >
      {isDarkMode ? (
        <svg className="w-6 h-6 text-yellow-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
          <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} 
                d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
        </svg>
      ) : (
        <svg className="w-6 h-6 text-gray-900" fill="none" viewBox="0 0 24 24" stroke="currentColor">
          <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" />
        </svg>
      )}
    </button>
  )
}
Enter fullscreen mode Exit fullscreen mode

Updating Our App Component

Update src/App.tsx to use dark mode classes:

import { ThemeToggle } from './components/ThemeToggle'

function App() {
  // ... existing task state and functions

  return (
    <div className="min-h-screen bg-gray-100 dark:bg-gray-900 py-8 transition-colors duration-200">
      <ThemeToggle />
      <div className="max-w-md mx-auto bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
        <h1 className="text-2xl font-bold text-center mb-6 dark:text-white">
          Task Manager
        </h1>

        <div className="flex mb-4">
          <input
            type="text"
            value={newTask}
            onChange={(e) => setNewTask(e.target.value)}
            className="flex-1 p-2 border rounded-l-lg focus:outline-none focus:border-blue-500
                     dark:bg-gray-700 dark:border-gray-600 dark:text-white"
            placeholder="Add a new task..."
            onKeyUp={(e) => {
              if (e.key === "Enter") addTask();
            }}
          />
          <button
            onClick={addTask}
            className="bg-blue-500 text-white px-4 rounded-r-lg hover:bg-blue-600
                     transition-colors duration-200"
          >
            Add
          </button>
        </div>

        <div className="space-y-2">
          {tasks.map(task => (
            <div
              key={task.id}
              className="flex items-center p-3 bg-gray-50 dark:bg-gray-700 rounded-lg"
            >
              <label className="flex items-center flex-1 cursor-pointer">
                <input
                  type="checkbox"
                  checked={task.completed}
                  onChange={() => toggleTask(task.id)}
                  className="mr-3 h-4 w-4 cursor-pointer accent-blue-500"
                />
                <span className={`flex-1 dark:text-white
                  ${task.completed ? 'line-through text-gray-500 dark:text-gray-400' : ''}`}>
                  {task.text}
                </span>
              </label>
              <button
                onClick={() => deleteTask(task.id)}
                className="text-red-500 hover:text-red-700 dark:text-red-400
                         dark:hover:text-red-300"
              >
                Delete
              </button>
            </div>
          ))}
        </div>
      </div>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

That's it! Here's what we've added:

  1. Theme context for managing dark mode state
  2. System theme detection
  3. Theme persistence using localStorage
  4. Smooth transitions between themes
  5. A cool toggle button with icons

Pro Tips:

  1. Add transitions to make it smooth:
/* In your index.css */
* {
  transition: background-color 0.2s ease, color 0.2s ease;
}
Enter fullscreen mode Exit fullscreen mode
  1. Handle initial flash of wrong theme:
<!-- In your index.html head -->
<script>
  if (localStorage.theme === 'dark' || (!localStorage.theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
    document.documentElement.classList.add('dark')
  }
</script>
Enter fullscreen mode Exit fullscreen mode
  1. Listen for system theme changes:
// Add to ThemeProvider
useEffect(() => {
  const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
  const handleChange = (e: MediaQueryListEvent) => {
    if (!localStorage.theme) {
      setIsDarkMode(e.matches)
    }
  }

  mediaQuery.addEventListener('change', handleChange)
  return () => mediaQuery.removeEventListener('change', handleChange)
}, [])
Enter fullscreen mode Exit fullscreen mode

And there you have it! A professional-looking dark mode implementation in just a few minutes. Try it out and customize the colors to match your app's theme! 🌙✨

Remember: Good dark mode isn't just about inverting colors - it's about creating a comfortable viewing experience in different lighting conditions. Feel free to adjust the colors until they feel right for your users!

This is the link to code repository

Top comments (0)