TrueSpec

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

Beginner

Toast Notification System

Custom toast component, queue management, animations, and accessibility

Frontend Patterns
Beginner

Responsive Layouts with CSS Grid

Auto-fit/fill, named areas, responsive breakpoints, and common patterns

Frontend Patterns
Intermediate

Next.js Server Actions Guide

Form mutations, revalidation, optimistic updates, and error handling

Frontend Patterns