Table of Contents

Create a folder with three files: index.html, style.css, and script.js.

1. HTML (index.html)

<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Persistent Dark & Light Mode Toggle</title>
  <link rel="stylesheet" href="style.css">
  <!-- Small Inline Script to Prevent White Flash on Dark Theme Page Load -->
  <script>
    (function () {
      const savedTheme = localStorage.getItem('site-theme');
      const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
      if (savedTheme === 'dark' || (!savedTheme && systemPrefersDark)) {
        document.documentElement.setAttribute('data-theme', 'dark');
      } else {
        document.documentElement.setAttribute('data-theme', 'light');
      }
    })();
  </script>
</head>
<body>

  <header class="navbar">
    <div class="logo">NitroX Web</div>
    
    <!-- Theme Toggle Switch -->
    <button id="themeToggle" class="theme-btn" aria-label="Toggle Dark Mode" aria-pressed="false">
      <!-- Sun Icon (Visible in dark mode) -->
      <svg class="icon sun-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364-6.364l-.707.707M6.343 17.657l-.707.707m12.728 0l-.707-.707M6.343 6.343l-.707-.707m12.728 12.728a9 9 0 11-12.728 0z" />
      </svg>
      <!-- Moon Icon (Visible in light mode) -->
      <svg class="icon moon-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
      </svg>
      <span id="toggleText" class="btn-text">Dark Mode</span>
    </button>
  </header>

  <main class="content-wrapper">
    <section class="info-card">
      <span class="badge">Feature Showcase</span>
      <h1>Experience Seamless Theme Switching</h1>
      <p>Click the toggle in the navigation bar to test the transitions. This theme system detects system preferences, preserves choices between page reloads using localStorage, and stops "flash-on-load" completely.</p>
      
      <div class="test-grid">
        <div class="grid-item">
          <h3>Local Storage</h3>
          <p>Your toggle choices are saved instantly to the visitor's browser memory.</p>
        </div>
        <div class="grid-item">
          <h3>No FOUC</h3>
          <p>An inline blocker Script halts "flash of unstyled content" dead in its tracks.</p>
        </div>
        <div class="grid-item">
          <h3>Auto OS Sync</h3>
          <p>Inherits System Settings automatically if the visitor has never chosen a custom scheme before.</p>
        </div>
      </div>
    </section>
  </main>

  <script src="script.js"></script>
</body>
</html>

2. CSS (style.css)

/* ==========================================================================
   CSS Custom Properties (Dark / Light Theme variables)
   ========================================================================== */
:root[data-theme="light"] {
  --bg-primary: #f8fafc;
  --bg-card: #ffffff;
  --border-color: #e2e8f0;
  --text-main: #0f172a;
  --text-muted: #475569;
  --accent-color: #4f46e5;
  --accent-hover: #4338ca;
  --nav-bg: rgba(255, 255, 255, 0.8);
  --badge-bg: rgba(79, 70, 229, 0.08);
  --badge-text: #4f46e5;
  --grid-item-bg: #f1f5f9;
  --shadow-sm: 0 1px 3px rgba(0,0,0,0.05);
  --shadow-lg: 0 10px 25px -5px rgba(15, 23, 42, 0.08);
  --switch-label: "Dark Mode";
}

:root[data-theme="dark"] {
  --bg-primary: #090d16;
  --bg-card: #131b2e;
  --border-color: #1e293b;
  --text-main: #f8fafc;
  --text-muted: #94a3b8;
  --accent-color: #10b981;
  --accent-hover: #059669;
  --nav-bg: rgba(19, 27, 46, 0.8);
  --badge-bg: rgba(16, 185, 129, 0.12);
  --badge-text: #10b981;
  --grid-item-bg: #1e293b;
  --shadow-sm: 0 1px 3px rgba(0,0,0,0.3);
  --shadow-lg: 0 15px 30px -10px rgba(0, 0, 0, 0.5);
  --switch-label: "Light Mode";
}

/* ==========================================================================
   Global & Reset Styling
   ========================================================================== */
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease;
}

body {
  font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background-color: var(--bg-primary);
  color: var(--text-main);
  line-height: 1.6;
  min-height: 100vh;
}

/* Blur & Backdrop Overlays for sticky Navbar */
.navbar {
  position: sticky;
  top: 0;
  z-index: 100;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1rem 2rem;
  background-color: var(--nav-bg);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--border-color);
}

.logo {
  font-weight: 800;
  font-size: 1.25rem;
  letter-spacing: -0.02em;
  color: var(--accent-color);
}

/* ==========================================================================
   Interactive Toggle Switch
   ========================================================================== */
.theme-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  background-color: var(--bg-primary);
  border: 1.5px solid var(--border-color);
  border-radius: 999px;
  padding: 0.5rem 1rem;
  color: var(--text-main);
  font-weight: 600;
  font-size: 0.875rem;
  cursor: pointer;
  box-shadow: var(--shadow-sm);
  outline: none;
}

.theme-btn:hover {
  border-color: var(--accent-color);
}

.theme-btn:focus-visible {
  box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.3);
}

.icon {
  width: 18px;
  height: 18px;
  stroke-width: 2px;
}

/* Toggle Visibility depending on state */
:root[data-theme="light"] .sun-icon { display: none; }
:root[data-theme="dark"] .moon-icon { display: none; }

/* ==========================================================================
   Main Layout & Content
   ========================================================================== */
.content-wrapper {
  max-width: 800px;
  margin: 3.5rem auto;
  padding: 0 1.5rem;
}

.info-card {
  background-color: var(--bg-card);
  border: 1px solid var(--border-color);
  border-radius: 16px;
  padding: 3rem;
  box-shadow: var(--shadow-lg);
}

.badge {
  display: inline-block;
  background-color: var(--badge-bg);
  color: var(--badge-text);
  font-size: 0.75rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  padding: 0.3rem 0.8rem;
  border-radius: 999px;
  margin-bottom: 1.25rem;
}

.info-card h1 {
  font-size: 2.25rem;
  font-weight: 800;
  letter-spacing: -0.03em;
  margin-bottom: 1rem;
}

.info-card p {
  color: var(--text-muted);
  font-size: 1.1rem;
  margin-bottom: 2.5rem;
  max-width: 650px;
}

/* Test Grid Features */
.test-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 1.5rem;
}

.grid-item {
  background-color: var(--grid-item-bg);
  padding: 1.5rem;
  border-radius: 10px;
  border: 1px solid transparent;
}

.grid-item:hover {
  border-color: var(--border-color);
}

.grid-item h3 {
  font-size: 1.05rem;
  font-weight: 700;
  margin-bottom: 0.5rem;
}

.grid-item p {
  font-size: 0.875rem;
  line-height: 1.5;
  color: var(--text-muted);
  margin-bottom: 0;
}

/* ==========================================================================
   Mobile Optimization
   ========================================================================== */
@media (max-width: 600px) {
  .info-card {
    padding: 2rem 1.5rem;
  }
  .info-card h1 {
    font-size: 1.75rem;
  }
}

3. JavaScript (script.js)

document.addEventListener('DOMContentLoaded', () => {
  const themeToggle = document.getElementById('themeToggle');
  const toggleText = document.getElementById('toggleText');

  // Helper function to set the visual text/label
  const updateToggleUI = (theme) => {
    if (theme === 'dark') {
      toggleText.textContent = 'Light Mode';
      themeToggle.setAttribute('aria-pressed', 'true');
    } else {
      toggleText.textContent = 'Dark Mode';
      themeToggle.setAttribute('aria-pressed', 'false');
    }
  };

  // 1. Establish the start state based on active theme
  const currentTheme = document.documentElement.getAttribute('data-theme');
  updateToggleUI(currentTheme);

  // 2. Click Handler - Switch themes with toggle animation & persistence
  themeToggle.addEventListener('click', () => {
    const currentTheme = document.documentElement.getAttribute('data-theme');
    let targetTheme = 'light';

    if (currentTheme === 'light') {
      targetTheme = 'dark';
    }

    // Update document attribute and save to local preference
    document.documentElement.setAttribute('data-theme', targetTheme);
    localStorage.setItem('site-theme', targetTheme);
    
    // Update label & accessible aria button attribute
    updateToggleUI(targetTheme);
  });

  // 3. Keep site updated if the OS preference changes in real-time
  window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
    // Only apply the OS change dynamically if user hasn't override manually
    if (!localStorage.getItem('site-theme')) {
      const systemTheme = e.matches ? 'dark' : 'light';
      document.documentElement.setAttribute('data-theme', systemTheme);
      updateToggleUI(systemTheme);
    }
  });
});

Prevent Dark Mode Flicker: A Bulletproof HTML, CSS & JS Theme Toggle Integration

A dark mode is no longer just a trend—it is a critical feature that saves battery, helps readers stay focused at night, and makes your web applications accessible.

However, many developers run into a notorious, unpolished visual bug: the “Flicker” or “Flash” on load (FOUC). This happens when a dark mode choice has been saved inside a user’s browser, but the page starts rendering as default light HTML before the external JavaScript loads, recognizes the storage, and toggles the CSS attributes.

In this guide, we will examine the standard setup for a modern Dark/Light theme toggle, how to prevent page flickers completely, and how to seamlessly recognize OS system configurations out of the box.

Why Most Theme Switches “Flicker” (and the Fix)

Normally, we bundle all JavaScript at the end of the <body> tag or run it within a defer block so it does not delay rendering.

<!-- Why this fails: The page is already visually loaded as "light mode" by the time script.js loads -->
<script src="script.js" defer></script>

To stop this white flash, we place a tiny, inline Blocking script directly inside the <head> of our index.html. Browsers execute script blocks in order as they parse the HTML. By checking localStorage and prefers-color-scheme in the head before any visible assets render, we can apply the data-theme="dark" attribute to the <html> or <body> instantaneously.

<script>
  (function () {
    const savedTheme = localStorage.getItem('site-theme');
    const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
    if (savedTheme === 'dark' || (!savedTheme && systemPrefersDark)) {
      document.documentElement.setAttribute('data-theme', 'dark');
    } else {
      document.documentElement.setAttribute('data-theme', 'light');
    }
  })();
</script>

Understanding the Multi-Tier Theme Architecture

Our dark mode relies on a three-tier design to offer maximum reliability:

  1. Specific Override (User Decision): When a reader clicks your mode toggle, their preference is written straight to localStorage under a unique string identifier (e.g. site-theme). Because the value never expires, they only have to make this choice once.
  2. Standard Fallback (System Settings): If the visitor is a new user with no historical saved overrides, the blocker script checks their active operating system settings via JavaScript’s window.matchMedia('(prefers-color-scheme: dark)') to mirror their desktop or mobile system.
  3. Transition-Aware Styling (CSS Variables): All color-dependent values use custom CSS variable tokens prefixed within [data-theme="light"] and [data-theme="dark"] scopes. The browser smoothly scales and transitions between them when JavaScript alters the attribute on the base elements.

Troubleshooting Tips

  • Unwanted Transitions During Flash-to-Load: If you notice an animated fade on page loads, don’t put transitions directly on the global body tag without limits. Use localized targets like .info-card or transition specific properties only: transition: background-color 0.3s ease, color 0.3s ease; as used in our stylesheet.
  • Storing Object States: Remember that localStorage can only read and write strings. If you want to store a larger scheme object later, serialize and deserialize them using JSON.stringify() and JSON.parse().