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.
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: [],
}
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
}
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>
)
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>
)
}
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>
)
}
That's it! Here's what we've added:
- Theme context for managing dark mode state
- System theme detection
- Theme persistence using localStorage
- Smooth transitions between themes
- A cool toggle button with icons
Pro Tips:
- Add transitions to make it smooth:
/* In your index.css */
* {
transition: background-color 0.2s ease, color 0.2s ease;
}
- 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>
- 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)
}, [])
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!
Top comments (0)