Implement Dark Mode With Theme Toggle
๐ Dark Mode Support and Theme Toggle Implementation
Hey guys! Let's dive into how we can add dark mode support to our application, complete with a theme toggle! This is a fantastic feature for users who prefer a darker interface, especially those working in low-light environments. We'll cover everything from automatic theme detection based on system preferences to manual toggling and persistent user preferences. This implementation ensures a seamless and accessible experience for everyone. So, let's get started!
The Why: Enhancing User Experience
Implementing dark mode isn't just about aesthetics; it's about improving the user experience. By allowing users to switch to a dark theme, we reduce eye strain, especially during evening or nighttime use. A dark theme also helps to conserve battery life on devices with OLED screens. Moreover, it enhances the overall visual appeal of the application, catering to users' diverse preferences. This feature is a great way to show that we care about our usersโ needs and wants.
User Story and Acceptance Criteria
To understand the scope, we follow this user story:
- As a user working in low-light environments, I want to switch to dark mode and have it remembered, so that I can reduce eye strain and improve my experience.
Our acceptance criteria ensure we cover all aspects:
- Dark mode color scheme implemented for entire UI.
- Light mode remains fully functional and default.
- User preference persisted in
localStorage:theme: 'light' | 'dark' | 'system'. - Automatic theme detection based on system preference (
prefers-color-scheme). - Theme toggle button in header/navbar.
- All components support both light and dark themes.
- Smooth transition between themes (no jarring color change).
- WebSocket updates respect current theme.
- Charts and graphs readable in both themes.
- WCAG AA contrast requirements met in both themes (4.5:1 for normal text).
- Buttons, links, form inputs visible in both themes.
- Images and icons work in both themes.
- No hardcoded colors - all use CSS variables.
- Works across all major browsers.
Technical Deep Dive: Frontend Implementation
Let's get our hands dirty with the technical details. We'll focus on the frontend changes, covering CSS variables, React context, and a theme toggle component.
CSS Variables: The Cornerstone of Theme Management
We'll use CSS variables (custom properties) to define our color schemes. This approach offers flexibility and maintainability. Here's a basic example:
/* Light theme (default) */
:root {
--color-bg-primary: #ffffff;
--color-bg-secondary: #f5f5f5;
--color-text-primary: #1a1a1a;
--color-text-secondary: #666666;
--color-border: #dddddd;
--color-button-bg: #007bff;
--color-button-text: #ffffff;
--color-hover-bg: #f0f0f0;
--color-card-bg: #ffffff;
--color-card-shadow: rgba(0, 0, 0, 0.1);
}
/* Dark theme */
[data-theme="dark"] {
--color-bg-primary: #1e1e1e;
--color-bg-secondary: #2d2d2d;
--color-text-primary: #e0e0e0;
--color-text-secondary: #a0a0a0;
--color-border: #404040;
--color-button-bg: #0056b3;
--color-button-text: #ffffff;
--color-hover-bg: #3a3a3a;
--color-card-bg: #2d2d2d;
--color-card-shadow: rgba(0, 0, 0, 0.5);
}
@media (prefers-color-scheme: dark) {
:root {
/* dark theme as default if system prefers it */
}
}
In the light theme (default), we define all the color variables. Then, in the dark theme, we override these variables using the [data-theme="dark"] attribute selector. This selector is applied to the <html> element when dark mode is active. The @media (prefers-color-scheme: dark) block allows the dark theme to be the default if the user's system preference is set to dark mode. This setup ensures that we can easily switch between themes by changing the data-theme attribute on the root element.
React Context: Managing the Theme Globally
To manage the theme across the application, we'll use React Context. This allows us to access and update the current theme from any component without prop drilling. Here's how we set it up:
// context/ThemeContext.tsx
import React, { createContext, useState, useEffect, useContext } from 'react';
interface ThemeContextType {
theme: 'light' | 'dark' | 'system';
effectiveTheme: 'light' | 'dark';
toggleTheme: () => void;
setTheme: (theme: 'light' | 'dark' | 'system') => void;
}
export const ThemeContext = createContext<ThemeContextType | null>(null);
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState(() => localStorage.getItem('theme') || 'system');
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
const effectiveTheme = theme === 'system' ? systemTheme : theme;
useEffect(() => {
document.documentElement.setAttribute('data-theme', effectiveTheme);
localStorage.setItem('theme', theme);
}, [theme, effectiveTheme]);
const toggleTheme = () => {
setTheme(prevTheme => {
if (prevTheme === 'light') return 'dark';
if (prevTheme === 'dark') return 'light';
return systemTheme === 'dark' ? 'light' : 'dark';
});
};
const value: ThemeContextType = {
theme,
effectiveTheme,
toggleTheme,
setTheme,
};
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
In this code, we create a ThemeContext and a ThemeProvider component. The ThemeProvider manages the current theme state, retrieves the user's saved preference from localStorage, and detects the system's preferred theme using prefers-color-scheme. It provides a toggleTheme function to switch between light and dark modes and stores the selected theme in localStorage for persistence. The useTheme hook is a custom hook that simplifies the use of the context in our components.
Theme Toggle Component: The User's Control
Next, we create a theme toggle button for users to manually switch between themes. This is a simple component:
// components/ThemeToggle.tsx
import React from 'react';
import { useTheme } from './ThemeContext';
export function ThemeToggle() {
const { effectiveTheme, toggleTheme } = useTheme();
return (
<button
onClick={toggleTheme}
aria-label="Toggle theme"
className="theme-toggle"
>
{effectiveTheme === 'light' ? '๐' : 'โ๏ธ'}
</button>
);
}
This component uses the useTheme hook to access the toggleTheme function. When the button is clicked, it calls toggleTheme, which updates the theme state. We display a moon icon (๐) for light mode and a sun icon (โ๏ธ) for dark mode.
Frontend: Affected Components and Color Palette Recommendations
Let's consider which components we need to update. Almost every visual component will be affected:
- Layout/Header: Navbar background, text color.
- Cards & Panels: Voting cards, session info.
- Forms & Inputs: Input fields, labels, placeholders.
- Charts: Recharts/Chart.js themes.
- Modals & Dialogs: Background, text, buttons.
- Tables: Rows, headers, borders.
- Sidebar (if exists): Navigation items.
- Buttons & Links: Hover states, active states.
Here are some color palette recommendations. It's crucial to ensure sufficient contrast for accessibility:
Light Theme:
- Background:
#ffffff - Text:
#1a1a1aor#2c3e50 - Accent:
#007bff - Borders:
#e1e4e8
Dark Theme:
- Background:
#1e1e1eor#0d1117 - Text:
#e1e8edor#c9d1d9 - Accent:
#58a6ff(lighter blue for readability) - Borders:
#30363d
Remember to use semantic color names (e.g., bg-primary, text-secondary) instead of literal color values for better maintainability.
Backend Changes (Optional)
While this is primarily a frontend feature, here's what we can do on the backend:
- Optional: Store user theme preference in the database
users.theme_preference. This provides better persistence if a user uses different browsers or devices. - Add an API endpoint:
PATCH /users/preferencesto persist the theme preference to the backend.
Testing: Ensuring Quality and Accessibility
Testing is vital to ensure our implementation is robust and accessible:
- Visual regression tests for both themes.
- Contrast ratio verification using tools like axe DevTools.
- Browser DevTools: toggle
prefers-color-schemesimulation. - Unit tests for
ThemeContext. - E2E tests: toggle theme, reload page, verify persistence.
- Test on different devices (phone, tablet, desktop).
Design Considerations and Responsive Design
Here are some additional considerations:
- Use semantic color names to avoid confusion.
- Ensure sufficient contrast ratios (WCAG AA) for all text and UI elements to meet accessibility standards.
- Test the implementation with color blindness simulators to identify any potential issues.
- Implement smooth CSS transitions when switching between themes to improve the user experience.
- Consider whether animations should be theme-aware (e.g., use different animation styles for light and dark modes).
For responsive design:
- The theme preference should sync across tabs and windows using the
storageevent listener for cross-tab sync.
Implementation Steps and Resources
- Create
ThemeContextwith system preference detection. - Create
ThemeProviderwrapper for the App component. - Define CSS variables for both themes.
- Add a theme toggle button to the header.
- Update all components to use CSS variables.
- Test contrast ratios using tools like WebAIM Contrast Checker.
- Add
localStoragepersistence. - Create tests.
- Update documentation.
Resources
- MDN: prefers-color-scheme: https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme
- React Dark Mode Guide: https://www.joshwcomeau.com/gatsby/dark-mode/
- WCAG Color Contrast: https://webaim.org/articles/contrast/
- CSS Variables (Custom Properties): https://developer.mozilla.org/en-US/docs/Web/CSS/--*
Conclusion
Adding dark mode support with a theme toggle is a valuable addition to our application. It enhances user experience, improves accessibility, and showcases our commitment to user preferences. By following these steps and best practices, we can create a beautiful and functional dark mode that delights our users. Happy coding, everyone!