Introduction/ Issue:
In modern web applications, users expect a seamless experience that adapts to their environment and preferences. One common requirement is supporting both light and dark themes. By default, many sites ship only a single, static stylesheet forcing users into a bright interface late at night or a dark interface in bright daylight. This mismatch can cause eye strain, reduce engagement, and even negatively impact accessibility.
Why we need to do / Cause of the issue:
- User Comfort:Dark mode reduces glare in low light; light mode improves readability in bright settings.
- System Preferences:Honors OS/browser color-scheme settings.
- Consistency & Flexibility:Keeps UI elements visually aligned and easier to maintain.
- Engagement:Personalization increases user satisfaction and retention.
How do we solve:
From terminal install react icons using Pip install react-icons
1. Create Theme Context:
File: src/context/ThemeContext.tsx
Purpose: Centralizes theme state (light / dark), exposes toggleTheme(), applies .app {theme} class, and persists choice to localStorage.
import React, { createContext, useContext, useState, type ReactNode } from ‘react’;type Theme = ‘light’ | ‘dark’;
interface ThemeContextProps {
theme: Theme;
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextProps | undefined>(undefined);
export const ThemeProvider = ({ children }: { children: ReactNode }) => {
// initialize from localStorage or prefers-color-scheme
const [theme, setTheme] = useState<Theme>(() => {
try {
const stored = localStorage.getItem(‘theme’);
if (stored === ‘light’ || stored === ‘dark’) return stored as Theme;
if (typeof window !== ‘undefined’ && window.matchMedia && window.matchMedia(‘(prefers-color-scheme: dark)’).matches) {
return ‘dark’;
}
} catch {
/* ignore */
}
return ‘light’;
});
const toggleTheme = () => {
setTheme(prev => {
const next = prev === ‘light’ ? ‘dark’ : ‘light’;
try {
localStorage.setItem(‘theme’, next);
} catch {
/* ignore */
}
return next;
});
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{/* theme class applied here so children inherit .app.light / .app.dark */}
<div className={`app ${theme}`}>{children}</div>
</ThemeContext.Provider>
);
};
export const useTheme = () => {
const context = useContext(ThemeContext);
if (!context) throw new Error(‘useTheme must be used within ThemeProvider’);
return context;
};
2. Wrap App with Provider :
File: src/main.tsx
Purpose: Wraps <App /> with <ThemeProvider> so every component can access useTheme().
import ReactDOM from ‘react-dom/client’;
import App from ‘./App’;
import { ThemeProvider } from ‘./context/ThemeContext’;
import ‘./index.css’;ReactDOM.createRoot(document.getElementById(‘root’)!).render(
<React.StrictMode>
<ThemeProvider>
<App />
</ThemeProvider>
</React.StrictMode>
);
3. Build Theme Toggle Button
File: src/components/ThemeToggle.tsx (+ ThemeToggle.css)
Purpose: Renders sun/moon icons (from react-icons) and toggles theme on click. Includes animation & accessibility attributes.
import { useTheme } from ‘../context/ThemeContext’;
import { FaSun, FaMoon } from ‘react-icons/fa’;
import ‘./ThemeToggle.css’;
const ThemeToggle = () => {
const { theme, toggleTheme } = useTheme();return (
<button
className=”theme-toggle”
onClick={toggleTheme}
aria-label=”Toggle theme”
title={theme === ‘light’ ? ‘Switch to dark mode’ : ‘Switch to light mode’}
>
{theme === ‘light’ ? <FaMoon className=”icon moon” /> : <FaSun className=”icon sun” />}
</button>
);
};export default ThemeToggle;
Themetoggle.css
background: none;
border: none;
cursor: pointer;
padding: 8px;
font-size: 1.5rem; /* a little smaller to fit navbar comfortably */
transition: transform 0.25s ease;
display: inline-flex;
align-items: center;
justify-content: center;
}
.theme-toggle:hover {
transform: scale(1.08);
}.icon {
transition: color 0.25s ease, transform 0.25s ease;
line-height: 1;
}/* colors for icons */
.sun {
color: #f39c12;
}.moon {
color: #2980b9;
}
4. Create Navbar (uses Toggle)
File: src/components/Navbar.tsx (+ Navbar.css)
Purpose: Fixed top bar with app title left and logo + ThemeToggle grouped on the right. Applies .navbar.light / .navbar.dark styles.
import { useTheme } from ‘../context/ThemeContext’;
import ThemeToggle from ‘./ThemeToggle’;
import ‘./Navbar.css’;const Navbar = () => {
const { theme } = useTheme();
return (
<nav className={`navbar ${theme}`}>
<h2 className=”app-title”>ThemeCrafter</h2>
<div className=”right-section”>
<h3>Theme
</h3>
<ThemeToggle />
</div>
</nav>
);
};export default Navbar;
Navbar.css
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 80px;
display: flex;
justify-content: space-between; /* title left, controls right */
align-items: center;
padding: 0 0.5rem;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
transition: background-color 0.4s ease, color 0.4s ease;
z-index: 1000;
}.app-title {
font-size: 1.6rem;
font-weight: 700;
margin: 0;
}.right-section {
display: flex;
align-items: center;
gap: 0.8rem;
}/* Light Theme Styles */.navbar.light {
background-color: #ffffff;
color: #000000;
}
/* Dark Theme Styles */
.navbar.dark {
background-color: #1e1e1e;
color: #ffffff;
}
5. Global & Theme Styles
File: src/index.css
Purpose: Full-page setup, gradient backgrounds for .app.light and .app.dark, pushes content below navbar, responsive tweaks.
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* hide scrollbar but allow scrolling (optional) */
body::-webkit-scrollbar { display: none; }
body { -ms-overflow-style: none; scrollbar-width: none; }
html, body {
height: 100%;
margin: 0;
padding: 0;
overflow: visible;
}/* light / dark gradients */.app.light {
background: linear-gradient(120deg, #f6f9fc, #b6d3f0);
color: #000000;
}.app.dark {
background: linear-gradient(135deg, #1f1c2c, #928dab);
color: #ffffff;
}
/* layout: push content below fixed navbar and center */
.app {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
padding-top: 80px; /* same as navbar height */
box-sizing: border-box;
}
/* headings */
h1 {
font-size: 3rem;
margin: 1rem 0;
}
main p { color: rgba(0,0,0,0.7); }
/* small responsive tweak */
@media (max-width: 480px) {
.app-title { font-size: 1.2rem; }
.logo { font-size: 1.2rem; }
.theme-toggle { font-size: 1.2rem; padding: 6px; }
}
6. Render App Content
File: src/App.tsx
Purpose: Renders <Navbar /> and main content (welcome heading). The .app wrapper is produced by ThemeProvider.
import Navbar from ‘./components/Navbar’;const App = () => {
return (
<>
<Navbar />
<main style={{ padding: ‘2rem’, width: ‘100%’, textAlign: ‘center’ }}>
<h1>Welcome to the Theme Crafter App!</h1>
<p>Use the toggle in the top-right to switch between light and dark themes.</p>
</main>
</>
);
};export default App;
7. Run the App and Test It Out:
Run the following command in your terminal:
This will start the development server. Once it’s running, open the provided local URL (e.g., http://localhost:5173/) in your browser.
You should now see your application with a theme toggle button.
Click the button to switch between Light and Dark themes.
Verify that the colors update instantly and that the change feels smooth and consistent across the entire UI.
Conclusion:
By introducing a dedicated ThemeContext, a reusable ThemeToggle button with sun/moon icons and animations, and themed CSS classes with fluid gradients, we’ve provided a scalable, maintainable solution for light/dark mode. Users gain control over their viewing experience, developers benefit from centralized styling logic, and the app stays visually consistent across all components. This approach not only resolves the immediate need for theme switching but also lays the groundwork for future theming extensions—such as high-contrast modes or custom color palettes—without touching every individual component.