Setting and Persisting Color Scheme Preferences with CSS and JavaScript

Implement light and dark mode with modern CSS features and minimal JavaScript for a performant, user-friendly theme experience

Modern web applications increasingly offer users the choice between light and dark color themes. This feature has moved from a nice-to-have enhancement to an expected standard, driven by user preferences for comfortable viewing in different lighting conditions and reduced eye strain during extended screen time. Implementing a robust color scheme preference system requires understanding several CSS features and a minimal JavaScript layer for persistence.

The approach outlined in this guide leverages modern CSS capabilities while keeping JavaScript to an absolute minimum--using it only to persist user choices across page loads and return visits. This results in better performance, works for users with JavaScript disabled, and provides a progressively enhanced experience that respects both system preferences and explicit user choices.

For professional web development services that implement these best practices, our team specializes in building modern, accessible web applications that prioritize user experience and performance.

The Foundation: Understanding prefers-color-scheme

The CSS prefers-color-scheme media feature represents one of the foundational tools for implementing color themes. This media feature detects whether a user has requested a light or dark color theme through their operating system or browser settings. When a user configures their system to use dark mode, the prefers-color-scheme media feature reflects this preference, allowing websites to automatically adapt their appearance without any user interaction required on the site itself.

The prefers-color-scheme feature accepts two values: light and dark. A value of light indicates that the user prefers a light interface or has not expressed an active preference. A value of dark indicates that the user has explicitly requested a dark interface through their system settings. This automatic detection means that websites can provide a thoughtful default experience that aligns with user preferences established at the operating system level.

Syntax

:root {
 /* Default light theme styles */
 --background-color: #ffffff;
 --text-color: #1a1a1a;
 --primary-color: #0066cc;
}

@media (prefers-color-scheme: dark) {
 :root {
 /* Dark theme overrides */
 --background-color: #1a1a1a;
 --text-color: #f0f0f0;
 --primary-color: #66b3ff;
 }
}

Implementing basic support for system preferences requires wrapping dark theme styles within a media query. This approach automatically serves the appropriate theme based on system settings, requiring no user interaction and no JavaScript. However, it provides no way for users to override the system preference and choose a different theme for your specific website.

For developers building custom web applications, understanding this foundation enables you to create experiences that respect user preferences from the operating system level while maintaining flexibility for future enhancements. Combined with CSS custom properties, you can create maintainable theming systems that scale across your entire application.

MDN Web Docs - prefers-color-scheme

Signaling Theme Support with the color-scheme Property

The color-scheme CSS property works in concert with prefers-color-scheme but serves a different purpose: it tells the browser which color schemes an element can be comfortably rendered in, allowing the browser to adjust its own UI elements accordingly. When you declare support for both light and dark modes using color-scheme, the browser updates form controls, scrollbars, and other native UI elements to match the current color scheme.

Setting color-scheme on the :root element or the html element accomplishes several things simultaneously. First, it signals to the browser that your page supports being viewed in light or dark mode. Second, it changes the default colors of the browser's own interface elements to match the respective operating system setting. This creates a more cohesive experience where both your website and the browser chrome feel unified.

:root {
 color-scheme: light dark;
}

You can also signal support for color scheme switching by adding a meta element in the head of your document, which gives user agents earlier notice that your page supports light and dark modes. This meta tag ensures that the browser can apply the correct theme even before the CSS is parsed and applied, preventing the flash of unstyled content that can occur when themes differ significantly.

When implementing responsive design patterns for modern applications, integrating the color-scheme property alongside your CSS framework ensures consistent theming across all interface elements. This approach is particularly valuable when building progressive web applications that need to provide native-like experiences across devices.

web.dev - Color themes with Baseline CSS features

Meta Tag for Early Theme Detection
<head>
 <meta name="color-scheme" content="light dark">
</head>

Building a Theme Toggle with Modern CSS

Modern CSS provides powerful selectors that enable theme toggling without JavaScript. The :has() pseudo-class, now supported across major browsers, allows you to style elements based on the state of other elements on the page. This capability transforms how we can implement theme toggles, moving away from JavaScript-driven class manipulation toward a more declarative approach.

A common pattern involves using a select menu or radio buttons to let users choose their preferred theme. With :has(), you can style the entire page based on the state of this control:

/* Default styles (light theme) */
:root {
 --background-color: #ffffff;
 --text-color: #1a1a1a;
}

:root:has(option[value="dark"]:checked) {
 /* Dark theme when dark option is selected */
 --background-color: #1a1a1a;
 --text-color: #f0f0f0;
}

This pattern can be combined with prefers-color-scheme to create a layered system that respects system preferences by default while allowing explicit overrides:

:root {
 color-scheme: light;
 --background-color: #ffffff;
 --text-color: #1a1a1a;
}

/* When user explicitly selects dark mode */
:root:has(option[value="dark"]:checked) {
 color-scheme: dark;
 --background-color: #1a1a1a;
 --text-color: #f0f0f0;
}

/* When system prefers dark AND user hasn't overridden */
@media (prefers-color-scheme: dark) {
 :root:has(option[value="system"]:checked),
 :root:not(:has(select)) {
 color-scheme: dark;
 --background-color: #1a1a1a;
 --text-color: #f0f0f0;
 }
}

This approach allows for three modes: light only, dark only, and system-preferred. Users can override the system preference, and the system preference only applies when the user has selected "system" mode or when no theme selector is present on the page.

For enterprise web applications requiring sophisticated theme management, this CSS-first approach provides a maintainable foundation that scales across large codebases. Learn more about building modern web interfaces that leverage these advanced CSS capabilities.

Smashing Magazine - Color Scheme Preferences

Persisting User Preferences Across Sessions

While CSS handles the visual aspects of theme switching, JavaScript plays an essential role in persisting user choices so they persist across page loads and return visits. The most common approach uses the browser's localStorage API to store the user's preference and retrieve it when the page loads.

The persistence strategy involves three key operations: storing the preference when the user makes a selection, reading the preference when the page loads, and applying the appropriate theme based on the stored value. This requires careful coordination between CSS and JavaScript to ensure the correct theme is applied immediately on page load.

// Store user preference when they make a selection
function setColorScheme(scheme) {
 localStorage.setItem('color-scheme', scheme);
 applyColorScheme(scheme);
}

// Apply the stored preference
function applyColorScheme(scheme) {
 const select = document.getElementById('theme-selector');
 if (select) {
 select.value = scheme;
 }
 
 // Update a data attribute for CSS selectors
 document.documentElement.setAttribute('data-color-scheme', scheme);
}

// Initialize on page load
function initColorScheme() {
 const stored = localStorage.getItem('color-scheme');
 const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
 
 if (stored) {
 applyColorScheme(stored);
 } else if (systemPrefersDark) {
 applyColorScheme('dark');
 }
}

The CSS then responds to the data attribute applied by JavaScript:

:root {
 color-scheme: light;
 --background-color: #ffffff;
}

:root[data-color-scheme="dark"] {
 color-scheme: dark;
 --background-color: #1a1a1a;
}

This separation of concerns keeps JavaScript focused on persistence and state management while allowing CSS to handle all visual presentation. The result is a clean architecture that leverages the strengths of both technologies.

Implementing this pattern in your frontend development workflow ensures that theme preferences remain consistent across user sessions without compromising performance or accessibility. For applications requiring deeper JavaScript integration, consider how localStorage patterns can extend to other user preference systems.

Explore related techniques for CSS-based interactions that enhance accessibility alongside your theme implementation.

Performance Considerations

Implementing color scheme preferences with performance in mind requires attention to several factors. First, the theme should be applied before the first paint to prevent a flash of incorrect colors. This can be achieved by including critical CSS inline and by using the color-scheme meta tag mentioned earlier.

For optimal performance, consider these strategies:

The color-scheme meta tag allows the browser to apply the correct color scheme before CSS is parsed, eliminating render-phase theme switching. This is particularly important for preventing layout shifts and visual flickering that can occur when the page renders with the wrong theme initially and then switches.

CSS custom properties (variables) for colors rather than separate style blocks makes theme switching more efficient. When variables change, browsers can efficiently update only the affected properties rather than re-evaluating entire rule sets. This approach aligns with CSS cascade layers for organizing styles effectively.

Minimizing JavaScript execution on the critical path ensures that theme application doesn't delay other rendering work. The initialization script should run as early as possible and complete quickly, with any additional functionality deferred.

Best Practices for Smooth Theme Transitions

Implementing smooth transitions between themes enhances the user experience and reduces the jarring effect of instant color changes. CSS provides the transition property for animating property changes, but some properties require special consideration when transitioning themes.

For background colors and text colors, straightforward transitions work well:

:root {
 --background-color: #ffffff;
 --text-color: #1a1a1a;
 transition: background-color 0.3s, color 0.3s;
}

:root[data-color-scheme="dark"] {
 --background-color: #1a1a1a;
 --text-color: #f0f0f0;
}

However, not all theme-related properties should be transitioned. Consider providing a transition-preference media query for users who prefer reduced motion:

@media (prefers-reduced-motion: reduce) {
 :root {
 transition: none;
 }
}

This approach respects user accessibility preferences while providing a polished visual experience for those who enjoy smooth transitions.

Key Implementation Points

System Preference Detection

Use prefers-color-scheme to detect and respond to OS-level theme preferences automatically without JavaScript.

Browser UI Integration

The color-scheme property ensures form controls, scrollbars, and native elements match your chosen theme.

JavaScript for Persistence Only

Keep JavaScript minimal--use it only to persist user choices across sessions using localStorage.

Smooth Transitions

Add CSS transitions for visual comfort, always respecting reduced-motion preferences.

Frequently Asked Questions

Sources

  1. MDN Web Docs - prefers-color-scheme - Official documentation covering the CSS media feature for detecting user system color scheme preferences.
  2. Smashing Magazine - Setting And Persisting Color Scheme Preferences With CSS And A Touch Of JavaScript - Comprehensive guide showing the modern approach using CSS :has() pseudo-class.
  3. web.dev - Color themes with Baseline CSS features - Google's guide on modern CSS features including color-scheme property and light-dark() function.

Need Help Implementing Color Schemes?

Our team specializes in building modern, performant web applications with best-practice implementations. Contact us to discuss how we can help you deliver exceptional user experiences.