Table of Contents
- Why Dark Mode Matters in 2025
- The Fundamentals: prefers-color-scheme
- Method 1: Pure CSS Dark Mode with Custom Properties
- Method 2: CSS Dark Mode Toggle with JavaScript
- Method 3: Persistent Dark Mode with localStorage
- Designing a Dark Theme Palette
- Dark Mode for Images and Media
- Dark Mode Accessibility
- Advanced Techniques
- Common Mistakes
- FAQ
- Conclusion
Why Dark Mode Matters in 2025
Dark mode is no longer a "nice to have" feature — it is an essential part of modern web design. By 2025, over 80% of operating systems (Windows, macOS, iOS, Android) offer system-wide dark mode, and users increasingly expect websites to respect their preference. A dark mode website CSS implementation directly impacts user satisfaction, accessibility, and even battery life on OLED screens.
Studies show that dark mode can reduce eye strain in low-light environments, improve readability for users with visual sensitivities like photophobia, and save up to 30% battery on mobile devices with OLED displays. For these reasons, implementing dark mode correctly is now a baseline expectation, not a premium feature.
In this comprehensive tutorial, you will learn multiple approaches to add dark mode to any website, from the simplest CSS-only method to a full-featured toggle with user preference persistence. By the end, you will know how to create a dark mode website with CSS that works across all devices and respects user preferences.
The Fundamentals: prefers-color-scheme
The easiest way to implement dark mode website CSS is using the prefers-color-scheme media query. This CSS media feature detects whether the user has set their operating system to light or dark mode and applies styles accordingly. It requires zero JavaScript and respects the user's system-wide preference.
/* Default light mode styles */
body {
background-color: #ffffff;
color: #1a1a2e;
}
/* Automatically applied when user prefers dark mode */
@media (prefers-color-scheme: dark) {
body {
background-color: #1a1a2e;
color: #e0e0e0;
}
}
This approach is supported by every modern browser — Chrome, Firefox, Safari, Edge — and works on all major operating systems. The browser reads the system preference and applies the appropriate styles without any user interaction on your website.
While this is the simplest method, it has a limitation: users cannot manually override the theme. If a user prefers light mode system-wide but wants your site in dark mode, they cannot switch without changing their entire OS setting. This is where Method 2 and Method 3 become valuable.
Method 1: Pure CSS Dark Mode with Custom Properties
The most scalable approach to create a dark mode website with CSS combines prefers-color-scheme with CSS custom properties (design tokens). This gives you a single source of truth for all colors and makes your entire theme change by updating only the custom property values.
:root {
/* Light theme tokens */
--color-bg-primary: #ffffff;
--color-bg-secondary: #f5f5f7;
--color-bg-card: #fafafa;
--color-text-primary: #1a1a2e;
--color-text-secondary: #555570;
--color-text-muted: #8888a0;
--color-border: #e0e0e8;
--color-accent: #6366f1;
--color-accent-hover: #4f46e5;
--shadow-card: 0 2px 8px rgba(0, 0, 0, 0.08);
--gradient-hero: linear-gradient(135deg, #6366f1, #8b5cf6);
}
@media (prefers-color-scheme: dark) {
:root {
/* Dark theme tokens */
--color-bg-primary: #0f0f1a;
--color-bg-secondary: #1a1a2e;
--color-bg-card: #1e1e36;
--color-text-primary: #f0f0f8;
--color-text-secondary: #a0a0b8;
--color-text-muted: #606078;
--color-border: #2a2a44;
--color-accent: #818cf8;
--color-accent-hover: #6366f1;
--shadow-card: 0 2px 8px rgba(0, 0, 0, 0.3);
--gradient-hero: linear-gradient(135deg, #818cf8, #a78bfa);
}
}
/* Use the tokens throughout your CSS */
body {
background-color: var(--color-bg-primary);
color: var(--color-text-primary);
}
.card {
background: var(--color-bg-card);
border: 1px solid var(--color-border);
box-shadow: var(--shadow-card);
}
.btn-primary {
background: var(--gradient-hero);
color: #fff;
}
With this approach, adding new themes or modifying existing ones is as simple as changing the token values. The PixelGlass UI Kit at https://sesemix.gumroad.com/l/twbtmc uses exactly this methodology with 100+ design tokens, making it trivial to support both light and dark themes across 5 complete templates.
The beauty of using CSS custom properties is that they cascade. You can override specific tokens at any level — a component, a page, or even inline — giving you fine-grained control without breaking the system.
Method 2: CSS Dark Mode Toggle with JavaScript
To let users manually switch between light and dark modes, you need a small amount of JavaScript. The simplest approach toggles a class on the <html> element and uses CSS to apply the theme based on the class.
The HTML Toggle Button
<button id="theme-toggle" aria-label="Toggle dark mode">
<span class="icon-light">☀️</span>
<span class="icon-dark">🌙</span>
</button>
The CSS with Toggle Support
:root {
/* Light theme (default) */
--color-bg-primary: #ffffff;
--color-text-primary: #1a1a2e;
}
/* Applied when user's system prefers dark AND no manual toggle set */
@media (prefers-color-scheme: dark) {
:root:not(.light-theme) {
--color-bg-primary: #0f0f1a;
--color-text-primary: #f0f0f8;
}
}
/* Manual dark theme override */
:root.dark-theme {
--color-bg-primary: #0f0f1a;
--color-text-primary: #f0f0f8;
}
/* Manual light theme override */
:root.light-theme {
--color-bg-primary: #ffffff;
--color-text-primary: #1a1a2e;
}
/* Hide toggle icons based on active theme */
:root:not(.dark-theme) .icon-dark,
:root.dark-theme .icon-light {
display: none;
}
.icon-light,
.icon-dark {
display: inline-block;
}
The JavaScript
const themeToggle = document.getElementById('theme-toggle');
const root = document.documentElement;
// Get user's system preference
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
// On page load, check for saved preference or use system default
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'dark') {
root.classList.add('dark-theme');
} else if (savedTheme === 'light') {
root.classList.add('light-theme');
} else if (prefersDark) {
root.classList.add('dark-theme');
}
themeToggle.addEventListener('click', () => {
if (root.classList.contains('dark-theme')) {
root.classList.remove('dark-theme');
root.classList.add('light-theme');
localStorage.setItem('theme', 'light');
} else {
root.classList.remove('light-theme');
root.classList.add('dark-theme');
localStorage.setItem('theme', 'dark');
}
});
This gives you the best of both worlds: the user's system preference is respected by default, but they can manually override it for your specific site. The choice is saved in localStorage so it persists across sessions.
Method 3: Persistent Dark Mode with localStorage
Expanding on Method 2, here is a production-ready dark mode implementation that respects system preferences, allows manual override, persists the choice, and avoids a flash of incorrect theme on page load.
The Theme Initialization Script (in <head>)
To prevent a flash of light mode before JavaScript runs, include this inline script in your <head> — before your CSS loads:
<script>
(function() {
const saved = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (saved === 'dark' || (saved === null && prefersDark)) {
document.documentElement.classList.add('dark-theme');
}
})();
</script>
This script runs synchronously before the browser paints anything, so the correct theme class is applied instantly. Your CSS then uses that class to apply the right custom properties, resulting in a seamless dark mode experience with zero flash.
Designing a Dark Theme Palette
Creating a good dark mode is not just inverting colors. A well-designed dark mode website CSS thoughtfully adjusts the entire visual experience. Here is how to approach color design for dark themes:
Don't Use Pure Black
Pure #000000 as a background creates uncomfortable contrast and causes text to appear to "vibrate" on the screen. Instead, use a very dark gray or dark blue-gray like #0f0f1a, #121212, or #1a1a2e. These provide a comfortable foundation that reduces eye strain.
Reduce Color Saturation
Colors that look vibrant and pleasant in light mode can appear harsh and glowing in dark mode. Reduce saturation by 15-30% for your dark theme palette. For example, a brand blue of #3b82f6 in light mode might become #60a5fa in dark mode — lighter and less saturated.
Adjust Shadow Depth
Shadows in dark mode are tricky. On a dark background, dark shadows disappear. Use lighter, colored shadows instead — subtle blue or purple tints that create depth without relying on black:
:root.dark-theme {
--shadow-card: 0 4px 20px rgba(99, 102, 241, 0.1);
--shadow-elevated: 0 8px 40px rgba(99, 102, 241, 0.15);
}
Recommended Dark Theme Palette
| Token | Light Value | Dark Value | Purpose |
|---|---|---|---|
| --bg-primary | #ffffff | #0f0f1a | Main page background |
| --bg-secondary | #f5f5f7 | #1a1a2e | Section backgrounds |
| --bg-card | #fafafa | #1e1e36 | Card and component backgrounds |
| --text-primary | #1a1a2e | #f0f0f8 | Main body text |
| --text-secondary | #555570 | #a0a0b8 | Subheadings and captions |
| --text-muted | #8888a0 | #606078 | Placeholder and disabled text |
| --border | #e0e0e8 | #2a2a44 | Dividers and borders |
| --accent | #6366f1 | #818cf8 | Links and interactive elements |
Dark Mode for Images and Media
Images designed for light mode often look too bright or washed out in dark mode. You have several options to handle this:
CSS Filter Adjustment
Apply subtle CSS filters to images in dark mode to reduce their brightness and increase contrast:
@media (prefers-color-scheme: dark) {
img:not(.no-dark-mode) {
filter: brightness(0.85) contrast(1.1);
}
}
Dark Mode Specific Images
Use the <picture> element with prefers-color-scheme media query in the source element to serve different image assets for dark mode:
<picture>
<source srcset="logo-dark.svg" media="(prefers-color-scheme: dark)">
<img src="logo-light.svg" alt="Company Logo">
</picture>
This is the ideal approach for logos, icons, and illustrations that fundamentally need different treatment in light and dark themes.
Dark Mode SVGs
For inline SVGs, use CSS custom properties inside the SVG to let them respond to theme changes automatically:
.icon {
fill: var(--color-text-primary);
stroke: var(--color-text-primary);
}
Dark Mode Accessibility
Contrast Ratios
Dark mode must meet the same WCAG 2.1 AA contrast requirements as light mode (4.5:1 for normal text, 3:1 for large text). A common mistake in dark mode website CSS is making text too dim — text like #888888 on a #121212 background fails contrast requirements. Always test your dark palette with contrast checkers.
Respect prefers-reduced-transparency
Some users have a system preference for reduced transparency. When creating dark mode CSS, especially if you use glassmorphism or translucent backgrounds, respect this setting:
@media (prefers-reduced-transparency: reduce) {
.glass-card {
backdrop-filter: none;
-webkit-backdrop-filter: none;
background: rgba(255, 255, 255, 0.95);
}
}
Don't Force Dark Mode
Always provide a way to switch back to light mode. Some users prefer light mode even on dark-friendly operating systems, particularly in bright environments or for reading long content.
Advanced Techniques
Dark Mode Transitions
Animating the transition between themes creates a polished experience. Apply a transition on the <html> element's background and color properties:
html {
transition: background-color 0.3s ease, color 0.3s ease;
}
Multiple Color Themes
Extend your design system to support more than two themes using data attributes:
:root[data-theme="light"] { /* light tokens */ }
:root[data-theme="dark"] { /* dark tokens */ }
:root[data-theme="sepia"] { /* sepia tokens */ }
:root[data-theme="high-contrast"] { /* high contrast tokens */ }
Automatic Dark Mode Scheduling
Use JavaScript to automatically switch to dark mode at sunset using the Sunrise/Sunset API or a hardcoded schedule:
const hour = new Date().getHours();
if (hour < 6 || hour > 19) {
document.documentElement.classList.add('dark-theme');
}
Common Mistakes
Mistake 1: Forgetting Inputs and Form Elements
Default form inputs (text fields, selects, checkboxes) often retain light backgrounds in dark mode. Always explicitly theme your form elements:
input, textarea, select {
background: var(--color-bg-secondary);
color: var(--color-text-primary);
border: 1px solid var(--color-border);
}
Mistake 2: Hardcoding Colors
Hardcoding any color value makes it impossible to change themes without searching through your entire codebase. Always use CSS custom properties for any color in your design system.
Mistake 3: Ignoring Third-Party Content
Embedded YouTube videos, Twitter embeds, or third-party widgets will not automatically adapt to your dark mode. Either wrap them in a container with CSS styling or accept the mismatch as a minor trade-off.
Mistake 4: Not Testing with Real Content
A dark theme that looks perfect in a demo often breaks when populated with real content — especially user-generated content that includes images, code blocks, and tables. Test your dark mode extensively with production content.
FAQ
Can I create a dark mode website with CSS only?
Yes. The prefers-color-scheme media query allows a pure CSS dark mode approach with no JavaScript required. However, adding JavaScript gives users the ability to manually toggle themes.
Does dark mode affect SEO?
No. Search engines evaluate content and HTML structure, not CSS theme preferences. Dark mode does not affect search rankings.
How do I prevent the white flash on page load?
Include a small inline script in your <head> that checks localStorage and applies the theme class before any content renders. This prevents the flash of incorrect theme.
Does dark mode save battery?
On OLED and AMOLED screens, yes — dark pixels require less power. On LCD screens, the savings are negligible. The actual impact varies by device, brightness level, and screen technology.
What is the best way to design dark mode colors?
Start with your light palette and adjust each color for dark backgrounds: use darker whites (light grays) for backgrounds, lighter darks (medium grays) for text, and reduce saturation for accent colors. Never use pure black or pure white.
Can I have different dark modes (Nord, Dracula, etc.)?
Absolutely. Use a data-theme attribute approach and define multiple token sets. Let users choose from theme presets stored in localStorage.
Conclusion
Learning how to create a dark mode website with CSS is an essential skill for any modern web developer. The combination of media queries, CSS custom properties, and minimal JavaScript gives you a robust, accessible, and user-friendly dark mode implementation that works on any device.
The key takeaways from this tutorial:
- Start with
prefers-color-schemefor automatic system-level dark mode support. - Use CSS custom properties (design tokens) as a single source of truth for all colors.
- Add a JavaScript toggle with
localStoragepersistence for user control. - Eliminate the flash of incorrect theme with an inline initialization script.
- Design your dark palette deliberately — never just invert colors.
- Test contrast, accessibility, and real content thoroughly.
If you want a ready-made dark mode implementation with a complete design system, the PixelGlass UI Kit includes 5 pre-built templates with full light and dark mode support, 100+ design tokens, and 50+ components — all for $9. Every template ships with a prefers-color-scheme dark theme, a toggle switch, and localStorage persistence built in.
Related articles: CSS Design System Tokens Guide | Tailwind CSS vs Traditional CSS Comparison