Why Every Website Needs a Dark Mode in 2026
Dark mode is no longer a nice-to-have feature. Users expect it. Operating systems, browsers, and apps all support it natively now, and visitors who land on a site without a dark option often bounce faster than you would think.
In this tutorial, you will learn exactly how to add dark mode to a website with CSS and JavaScript. We will cover everything from CSS custom properties and a lightweight toggle function to storing the user preference in localStorage and detecting the system-level color scheme automatically.
By the end, you will have a production-ready dark mode implementation you can drop into any project, whether it is a static HTML site, a WordPress theme, or a JavaScript framework.
What You Will Need
- Basic knowledge of HTML, CSS, and JavaScript
- A website or a simple HTML page to test on
- A modern code editor (VS Code, Sublime Text, etc.)
No libraries. No frameworks. No dependencies. Just clean CSS and vanilla JavaScript.
Step 1: Define Your Color Palette with CSS Custom Properties
The foundation of any solid dark mode implementation is CSS custom properties (also called CSS variables). Instead of hard-coding colors throughout your stylesheet, you define them once on the :root selector and reference them everywhere else.
Light Theme Variables (Default)
:root {
--color-bg: #ffffff;
--color-surface: #f4f4f5;
--color-text-primary: #1a1a2e;
--color-text-secondary: #555555;
--color-accent: #2563eb;
--color-border: #e0e0e0;
--shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
Dark Theme Variables
[data-theme="dark"] {
--color-bg: #121212;
--color-surface: #1e1e2e;
--color-text-primary: #e4e4e7;
--color-text-secondary: #a1a1aa;
--color-accent: #60a5fa;
--color-border: #2e2e3a;
--shadow: 0 2px 8px rgba(0, 0, 0, 0.32);
}
Notice that we are using a data-theme attribute on the HTML element rather than a class. This approach is clean, semantic, and easy to target in both CSS and JavaScript.
Apply the Variables in Your Stylesheet
body {
background-color: var(--color-bg);
color: var(--color-text-primary);
font-family: system-ui, sans-serif;
line-height: 1.6;
transition: background-color 0.3s ease, color 0.3s ease;
}
.card {
background-color: var(--color-surface);
border: 1px solid var(--color-border);
box-shadow: var(--shadow);
padding: 1.5rem;
border-radius: 8px;
}
a {
color: var(--color-accent);
}
p {
color: var(--color-text-secondary);
}
The transition property on the body ensures a smooth visual transition between light and dark themes instead of an abrupt flash.
Step 2: Create the Dark Mode Toggle Button in HTML
Add a simple button to your header or navigation bar. You can style it however you like, but here is a minimal, accessible version:
<button id="theme-toggle" aria-label="Toggle dark mode" title="Toggle dark mode">
<span class="icon-sun">☀</span>
<span class="icon-moon">☾</span>
</button>
Basic Toggle Button CSS
#theme-toggle {
background: none;
border: 2px solid var(--color-border);
border-radius: 50%;
width: 44px;
height: 44px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
color: var(--color-text-primary);
transition: border-color 0.3s ease;
}
[data-theme="dark"] .icon-sun {
display: none;
}
[data-theme="dark"] .icon-moon {
display: inline;
}
:root .icon-moon {
display: none;
}
:root .icon-sun {
display: inline;
}
This hides the moon icon in light mode and the sun icon in dark mode, giving the user a clear visual cue about what will happen when they click.
Step 3: Write the JavaScript Toggle Function
Here is the core JavaScript that ties everything together. It is fewer than 30 lines and handles three critical tasks:
- Toggling the
data-themeattribute between light and dark - Saving the user preference in
localStorage - Detecting the operating system color scheme on first visit
(function () {
const toggleBtn = document.getElementById('theme-toggle');
const htmlEl = document.documentElement;
// Determine the initial theme
function getPreferredTheme() {
const stored = localStorage.getItem('theme');
if (stored) return stored;
return window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
}
// Apply theme to the document
function applyTheme(theme) {
htmlEl.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
}
// Set theme on page load
applyTheme(getPreferredTheme());
// Toggle on button click
toggleBtn.addEventListener('click', function () {
const current = htmlEl.getAttribute('data-theme');
const next = current === 'dark' ? 'light' : 'dark';
applyTheme(next);
});
// Listen for system preference changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function (e) {
if (!localStorage.getItem('theme')) {
applyTheme(e.matches ? 'dark' : 'light');
}
});
})();
Let us break down what each part does.
getPreferredTheme()
This function checks localStorage first. If the user has already chosen a theme during a previous visit, we respect that choice. If no preference is stored, we fall back to the operating system setting using window.matchMedia('(prefers-color-scheme: dark)').
applyTheme(theme)
Sets the data-theme attribute on the <html> element and writes the preference to localStorage so it persists across page loads and sessions.
System Preference Change Listener
If the user changes their OS from light to dark (or vice versa) while your page is open, this event listener updates the theme in real time, but only if the user has not manually overridden the theme.
Step 4: Prevent the Flash of Wrong Theme (FOWT)
One common problem is a brief flash of the light theme before the JavaScript runs. To fix this, add a small inline script in the <head> of your HTML, before any stylesheets load:
<script>
(function () {
var theme = localStorage.getItem('theme');
if (!theme) {
theme = window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
}
document.documentElement.setAttribute('data-theme', theme);
})();
</script>
Because this runs synchronously before the browser paints the page, the correct theme is applied instantly. No flash.
Step 5: Handle Images, SVGs, and Media in Dark Mode
Colors are only part of the story. You may also need to adjust images, logos, and embedded content. Here are a few techniques:
| Element | Technique | Example |
|---|---|---|
| Logo | Swap src with a <picture> element or use CSS filter: invert(1) |
[data-theme="dark"] .logo { filter: invert(1); } |
| Inline SVG | Use currentColor for fills and strokes |
<svg fill="currentColor">...</svg> |
| Photos | Slightly reduce brightness to avoid glare on dark backgrounds | [data-theme="dark"] img { filter: brightness(0.85); } |
| Embedded iframes | Use the embed’s own dark mode parameter if available | YouTube: &theme=dark |
Step 6: Add Smooth Transitions Between Themes
We already added a transition on the body, but for a polished experience, apply transitions to every element that uses your custom properties:
*,
*::before,
*::after {
transition: background-color 0.3s ease,
color 0.3s ease,
border-color 0.3s ease,
box-shadow 0.3s ease;
}
Performance tip: This universal selector transition works fine for most sites, but if you have thousands of DOM elements or complex animations, consider applying transitions only to specific components instead.
Step 7: Test Your Dark Mode Implementation
Before shipping, test these scenarios:
- First visit with OS set to light mode – should load light theme
- First visit with OS set to dark mode – should load dark theme
- Manual toggle – should switch theme and save to localStorage
- Return visit – should remember the user’s last choice
- Clearing localStorage – should fall back to OS preference
- Changing OS preference while page is open – should update only if no manual override
You can simulate the prefers-color-scheme media query in Chrome DevTools under Rendering > Emulate CSS media feature prefers-color-scheme.
Complete Code: Copy-and-Paste Dark Mode Solution
Here is the full, minimal implementation you can paste into any project:
HTML (head section)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Website</title>
<script>
(function () {
var t = localStorage.getItem('theme');
if (!t) {
t = window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark' : 'light';
}
document.documentElement.setAttribute('data-theme', t);
})();
</script>
<link rel="stylesheet" href="style.css">
</head>
HTML (body section)
<body>
<header>
<nav>
<span>My Site</span>
<button id="theme-toggle" aria-label="Toggle dark mode">
<span class="icon-sun">☀</span>
<span class="icon-moon">☾</span>
</button>
</nav>
</header>
<main>
<h1>Welcome to my dark-mode-ready website</h1>
<p>Toggle the button above to switch themes.</p>
</main>
<script src="theme.js"></script>
</body>
</html>
CSS (style.css)
:root {
--color-bg: #ffffff;
--color-surface: #f4f4f5;
--color-text-primary: #1a1a2e;
--color-text-secondary: #555555;
--color-accent: #2563eb;
--color-border: #e0e0e0;
}
[data-theme="dark"] {
--color-bg: #121212;
--color-surface: #1e1e2e;
--color-text-primary: #e4e4e7;
--color-text-secondary: #a1a1aa;
--color-accent: #60a5fa;
--color-border: #2e2e3a;
}
body {
margin: 0;
background-color: var(--color-bg);
color: var(--color-text-primary);
font-family: system-ui, sans-serif;
transition: background-color 0.3s ease, color 0.3s ease;
}
header {
background-color: var(--color-surface);
border-bottom: 1px solid var(--color-border);
padding: 1rem 2rem;
}
nav {
display: flex;
justify-content: space-between;
align-items: center;
}
#theme-toggle {
background: none;
border: 2px solid var(--color-border);
border-radius: 50%;
width: 44px;
height: 44px;
cursor: pointer;
font-size: 1.2rem;
color: var(--color-text-primary);
}
[data-theme="dark"] .icon-sun { display: none; }
[data-theme="dark"] .icon-moon { display: inline; }
:root .icon-moon { display: none; }
:root .icon-sun { display: inline; }
JavaScript (theme.js)
(function () {
var toggleBtn = document.getElementById('theme-toggle');
var htmlEl = document.documentElement;
function getPreferredTheme() {
var stored = localStorage.getItem('theme');
if (stored) return stored;
return window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark' : 'light';
}
function applyTheme(theme) {
htmlEl.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
}
applyTheme(getPreferredTheme());
toggleBtn.addEventListener('click', function () {
var current = htmlEl.getAttribute('data-theme');
applyTheme(current === 'dark' ? 'light' : 'dark');
});
window.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', function (e) {
if (!localStorage.getItem('theme')) {
applyTheme(e.matches ? 'dark' : 'light');
}
});
})();
Common Mistakes to Avoid
- Hard-coding colors instead of using CSS variables. This makes switching themes nearly impossible without duplicating your entire stylesheet.
- Placing the theme detection script at the bottom of the page. This causes the flash of wrong theme (FOWT) problem we discussed above.
- Forgetting about third-party components. If you use embedded widgets, iframes, or third-party UI libraries, make sure they also respond to your theme.
- Ignoring accessibility. Ensure your dark mode color combinations meet WCAG contrast requirements. Use tools like the WebAIM Contrast Checker to verify.
- Not testing on mobile devices. Dark mode behavior can vary between iOS Safari and Android Chrome, especially regarding the system-level preference detection.
Bonus: Using the CSS light-dark() Function
Modern browsers now support the light-dark() CSS function, which lets you specify both color values inline without needing a separate selector block:
:root {
color-scheme: light dark;
}
body {
background-color: light-dark(#ffffff, #121212);
color: light-dark(#1a1a2e, #e4e4e7);
}
This is a newer CSS feature with growing browser support. It works well for simpler sites, but the CSS custom properties approach we covered above gives you more control and is easier to toggle via JavaScript.
Frequently Asked Questions
Does adding dark mode affect SEO?
Dark mode itself does not directly affect search rankings. However, it improves user experience, which can reduce bounce rates and increase time on page. These behavioral signals may indirectly benefit your SEO.
Can I add dark mode to a WordPress site without a plugin?
Yes. You can paste the CSS variables into your theme’s stylesheet (or the Additional CSS section in the Customizer) and add the JavaScript to your theme’s footer or header. The same code from this tutorial works on WordPress without any modifications.
What if the user has JavaScript disabled?
If JavaScript is disabled, the inline script in the <head> will not run. In that case, you can use a pure CSS fallback with the @media (prefers-color-scheme: dark) media query to apply dark styles based on the OS setting. The toggle button will not work, but the user will still get the correct theme.
Should I default to light or dark mode?
Best practice is to default to the user’s operating system preference using prefers-color-scheme. If no preference is detectable, defaulting to light mode is the safest choice since it is what most users expect.
How do I add a third option like “system” alongside light and dark?
Add a three-way toggle that sets a value of “system” in localStorage. When the value is “system” (or when localStorage is empty), read the OS preference with window.matchMedia. When the value is “light” or “dark”, apply that theme directly regardless of the OS setting.
Will the transition cause performance issues?
For most websites, transitioning background-color, color, and border-color is perfectly smooth. These are inexpensive properties for browsers to animate. Avoid transitioning layout properties like width or height at the same time.
Wrapping Up
Adding dark mode to a website with CSS and JavaScript is one of the highest-impact improvements you can make for user experience with relatively little code. To summarize the key steps:
- Define your color palette using CSS custom properties
- Create a second set of variables under a
[data-theme="dark"]selector - Build a toggle button and wire it up with a small JavaScript function
- Store the user preference in
localStorage - Detect the system-level color scheme with
prefers-color-scheme - Prevent the flash of wrong theme with an inline head script
- Add smooth CSS transitions for a polished feel
If you need help implementing dark mode on your website or want a complete UI overhaul, get in touch with us at AHT Design. We build fast, accessible, and beautifully themed websites for businesses of all sizes.




