Dark Mode Toggle (CSS + JS)
System preference detection, localStorage persistence, smooth transitions
What You’ll Build
After following this guide, you will have a working implementation of dark mode toggle (css + js) in your project. Add a dark mode toggle that respects the user system preference by default, allows manual override, persists the choice in localStorage, and transitions smoothly. Uses CSS custom properties for theming — no framework needed.
Use Cases & Problems Solved
- Implement interactive UI features that users expect from modern apps
- Follow established patterns that scale and remain maintainable
- Reduce boilerplate and avoid common frontend pitfalls
Prerequisites
- Basic HTML, CSS, and JavaScript
Step-by-Step Implementation
CSS custom properties for theming
The following snippet shows how to css custom properties for theming. Copy this into your project and adjust the values for your environment.
:root {
--bg: #ffffff;
--text: #1a1a2e;
--card-bg: #f8f9fa;
--border: #e0e0e0;
--accent: #6366f1;
}
[data-theme="dark"] {
--bg: #0f0f23;
--text: #e4e4e7;
--card-bg: #1a1a2e;
--border: #2a2a3e;
--accent: #818cf8;
}
body {
background: var(--bg);
color: var(--text);
transition: background-color 0.3s ease, color 0.3s ease;
}
.card {
background: var(--card-bg);
border: 1px solid var(--border);
}
JavaScript toggle with system preference and persistence
The following snippet shows how to javascript toggle with system preference and persistence. Copy this into your project and adjust the values for your environment.
function initTheme() {
const saved = localStorage.getItem('theme');
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const theme = saved || (systemPrefersDark ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', theme);
}
function toggleTheme() {
const current = document.documentElement.getAttribute('data-theme');
const next = current === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', next);
localStorage.setItem('theme', next);
}
// Initialize on page load
initTheme();
// Listen for system preference changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
if (!localStorage.getItem('theme')) { // Only if user hasn't manually chosen
document.documentElement.setAttribute('data-theme', e.matches ? 'dark' : 'light');
}
});
⚠️ Don’t Do This
❌ Flash of wrong theme on page load (FART — Flash of inAccurate coloR Theme)
// Theme set AFTER page renders — user sees white flash in dark mode!
document.addEventListener('DOMContentLoaded', () => {
initTheme(); // Too late! Page already rendered with default theme
});
✅ Set theme in a blocking script in before body renders
<!-- In <head>, BEFORE any CSS loads -->
<script>
const t = localStorage.getItem('theme') ||
(matchMedia('(prefers-color-scheme:dark)').matches ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', t);
</script>
Testing
Verify your implementation with these tests:
// __tests__/dark-mode-toggle-css-js-.test.ts
import { describe, it, expect } from 'vitest';
describe('Dark Mode Toggle (CSS + JS)', () => {
it('should initialize without errors', () => {
// Test that the setup completes successfully
expect(() => setup()).not.toThrow();
});
it('should handle the primary use case', async () => {
const result = await execute();
expect(result).toBeDefined();
expect(result.success).toBe(true);
});
it('should handle edge cases', async () => {
// Test with empty/null input
const result = await execute(null);
expect(result.error).toBeDefined();
});
});
Verification
# Toggle dark mode on/off — should transition smoothly
# Refresh the page — preference should persist
# Change system dark mode setting — should auto-follow
# No white flash on page load in dark mode Related Specs
Toast Notification System
Custom toast component, queue management, animations, and accessibility
Responsive Layouts with CSS Grid
Auto-fit/fill, named areas, responsive breakpoints, and common patterns
Next.js Server Actions Guide
Form mutations, revalidation, optimistic updates, and error handling