HTML Setup
<button id="theme-toggle" aria-label="Toggle dark mode">
<span class="icon-sun">☀️</span>
<span class="icon-moon">🌙</span>
</button>
/* Show correct icon based on current theme */
[data-theme="light"] .icon-moon { display: none; }
[data-theme="dark"] .icon-sun { display: none; }
CSS Variables
[data-theme="light"] {
--bg: #ffffff;
--text: #0f172a;
--surface: #f1f5f9;
--border: #e2e8f0;
}
[data-theme="dark"] {
--bg: #060b14;
--text: #dde8f8;
--surface: #0b1220;
--border: #0f1e32;
}
body {
background: var(--bg);
color: var(--text);
transition: background 0.3s ease, color 0.3s ease;
}
JavaScript Logic
const toggle = document.getElementById('theme-toggle');
toggle.addEventListener('click', () => {
const current = document.documentElement.getAttribute('data-theme');
const next = current === 'dark' ? 'light' : 'dark';
applyTheme(next);
localStorage.setItem('theme', next);
});
function applyTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
}
Initialisation — No Flash on Load
This must go in <head> before CSS loads to prevent a flash of the wrong theme:
<script>
(function() {
const saved = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const theme = saved || (prefersDark ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', theme);
})();
</script>
✅ Result: On first visit, the user's OS preference is used. After they toggle, their choice is saved and applied immediately on every subsequent visit — no flash, no delay.