How to Create a Dark Mode Website with CSS — Complete 2025 Tutorial

Learn exactly how to create a dark mode website with CSS — from basic prefers-color-scheme to advanced toggle switches with localStorage persistence. Includes full code examples.

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

TokenLight ValueDark ValuePurpose
--bg-primary#ffffff#0f0f1aMain page background
--bg-secondary#f5f5f7#1a1a2eSection backgrounds
--bg-card#fafafa#1e1e36Card and component backgrounds
--text-primary#1a1a2e#f0f0f8Main body text
--text-secondary#555570#a0a0b8Subheadings and captions
--text-muted#8888a0#606078Placeholder and disabled text
--border#e0e0e8#2a2a44Dividers and borders
--accent#6366f1#818cf8Links 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-scheme for 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 localStorage persistence 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

Want production-ready templates?

PixelGlass UI Kit includes 5 templates, 50+ components, and 100+ design tokens — all for $9.

Get PixelGlass UI Kit →