📚 Light/Dark Mode with CSS Modules
This tutorial teaches you how to:
- Build a light/dark mode toggle using
useStateand CSS Modules - Understand and solve prop drilling using React Context
- Add and use a global CSS file in a React project
- Understand how
useContextworks internally - Use CSS variables and
bodyclass for scalable theming
✅ Part 1: Light/Dark Mode using CSS Modules and useState
1. Project Setup
npm create vite@latest theme-toggle-demo --template react
cd theme-toggle-demo
npm install
npm run dev
2. Folder Structure
src/
├── App.jsx
├── App.module.css
├── ThemeToggle.jsx
├── ThemeToggle.module.css
3. App.module.css
.app {
font-family: sans-serif;
padding: 2rem;
min-height: 100vh;
transition: background-color 0.3s, color 0.3s;
}
.card {
background-color: var(--card-bg);
padding: 1rem;
border-radius: 6px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
4. ThemeToggle.module.css
.button {
padding: 0.5rem 1rem;
border: 2px solid currentColor;
background: transparent;
color: inherit;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
}
5. ThemeToggle.jsx
import styles from './ThemeToggle.module.css';
export default function ThemeToggle({ theme, toggleTheme }) {
return (
<button className={styles.button} onClick={toggleTheme}>
Switch to {theme === 'light' ? 'Dark' : 'Light'} Mode
</button>
);
}
6. App.jsx (with CSS variables)
import { useState, useEffect } from 'react';
import styles from './App.module.css';
import ThemeToggle from './ThemeToggle';
function App() {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme((prev) => (prev === 'light' ? 'dark' : 'light'));
};
useEffect(() => {
document.body.className = theme;
}, [theme]);
return (
<div className={styles.app}>
<h1>Hello React</h1>
<p>This app uses CSS variables for a scalable theme system.</p>
<ThemeToggle theme={theme} toggleTheme={toggleTheme} />
<div className={styles.card}>Themed content box</div>
</div>
);
}
export default App;
➕ Bonus: Adding a Global CSS File
Include a global CSS file for base styles (like vars, fonts, resets, or utility classes).
1. Create src/global.css
:root {
--bg: #ffffff;
--fg: #000000;
--card-bg: #f9f9f9;
}
body.dark {
--bg: #1e1e1e;
--fg: #f5f5f5;
--card-bg: #2a2a2a;
}
body {
margin: 0;
font-family: system-ui, sans-serif;
background-color: var(--bg);
color: var(--fg);
transition: background-color 0.3s, color 0.3s;
}
2. Import it once in your entry point (usually main.jsx or main.tsx)
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './global.css';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
This global stylesheet will apply to your whole app and supports scalable theming using CSS variables.
⚠️ Problem: Prop Drilling
What if you need the theme in many nested components? Passing props through many layers becomes messy.
<App>
<Header theme={theme} />
<Navbar theme={theme} />
<UserMenu theme={theme} />
This is called prop drilling.
🔧 Part 2: Solving Prop Drilling with useContext
1. Create a context file ThemeContext.jsx
import { createContext, useContext, useState, useEffect } from 'react';
const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme((prev) => (prev === 'light' ? 'dark' : 'light'));
};
useEffect(() => {
document.body.className = theme;
}, [theme]);
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
return useContext(ThemeContext);
}
What is useTheme()?
This is a custom React hook. A custom hook is just a JavaScript function that:
- Starts with the word
use - Calls built-in React hooks (like
useContext,useState, etc.)
In this case, useTheme() is a convenience function that wraps useContext(ThemeContext) so that any component can simply write:
const { theme, toggleTheme } = useTheme();
instead of repeating:
const { theme, toggleTheme } = useContext(ThemeContext);
This improves readability and keeps your components clean.
2. Wrap your app in ThemeProvider
Edit main.jsx:
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { ThemeProvider } from './ThemeContext';
import './global.css';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<ThemeProvider>
<App />
</ThemeProvider>
</React.StrictMode>
);
3. Use context in App.jsx
import styles from './App.module.css';
import ThemeToggle from './ThemeToggle';
import { useTheme } from './ThemeContext';
function App() {
return (
<div className={styles.app}>
<h1>Hello React</h1>
<ThemeToggle />
<div className={styles.card}>Themed content box</div>
</div>
);
}
export default App;
4. Update ThemeToggle.jsx
import styles from './ThemeToggle.module.css';
import { useTheme } from './ThemeContext';
export default function ThemeToggle() {
const { theme, toggleTheme } = useTheme();
return (
<button className={styles.button} onClick={toggleTheme}>
Switch to {theme === 'light' ? 'Dark' : 'Light'} Mode
</button>
);
}
🤔 How useContext Works
When you create a context with createContext(), you’re telling React: “I want to be able to share this value with any component in the tree without passing it through props manually.”
ThemeContext.Providermakes the value available to all components inside it.useContext(ThemeContext)allows a child component to access that value directly.
So this:
<ThemeContext.Provider value={{ theme, toggleTheme }}>
<App />
</ThemeContext.Provider>
Makes the theme and toggleTheme available anywhere inside <App /> using:
const { theme, toggleTheme } = useContext(ThemeContext);
This solves the prop drilling problem because you don’t need to pass theme through every component manually.
🚀 Summary
- CSS Modules keep styles scoped to each component.
useStatecan toggle themes simply, but prop drilling becomes an issue.useContextis a clean and scalable way to share state across the app without manually passing props.- A global CSS file can be used for general layout or browser reset styles.
- CSS variables combined with a
bodyclass make it easy to handle many theme-based style rules. useContextworks by giving components direct access to shared values defined by a Provider.