Mastering Dark Mode with CSS Variables: A Step‑by‑Step Guide

This article explains how to implement a flexible dark‑mode theme using modern CSS custom properties, covering variable definition, runtime theme switching, Sass/Less integration, media‑query detection, image handling, event‑bus notifications, polyfills for older browsers, and the challenges of color mapping.

Huawei Cloud Developer Alliance
Huawei Cloud Developer Alliance
Huawei Cloud Developer Alliance
Mastering Dark Mode with CSS Variables: A Step‑by‑Step Guide

Theme Switcher Development

Modern browsers that support CSS custom properties (variables) allow developers to add or change themes at runtime, avoiding the need for pre‑compiled CSS files. Define variables with --var-name: value and retrieve them using var(--var-name, fallback). Example:

<!--html-->
<div><p>text</p></div>
/* css */
div { --my-color: red; border: 1px solid var(--my-color); }
p { color: var(--my-color); }

Placing variables on :root makes them available throughout the document.

Runtime Theme Switching

Insert or update a <style> element in the document head with new variable values, e.g., :root{--color1:#111;--color2:#222;}. Use innerText to replace the CSS when the theme changes. Provide default values to avoid missing colors before data loads, such as p { color: var(--color1, #fff); }.

Sass/Less Integration

To manage many variables, define them in Sass/Less and map them to CSS variables with fallbacks:

// before (Sass/Less)
$brand-primary: #5e7ce0;
// after (CSS variables)
$brand-primary: var(--brand-primary, #5e7ce0);

Note that once a value is wrapped in var(), Sass/Less color functions cannot operate on it directly.

Media Query Detection

Use the prefers-color-scheme media query to automatically apply light or dark themes based on the user's system setting:

@media (prefers-color-scheme: light) {
  :root{--color1:#fff;--color2:#eee;}
}
@media (prefers-color-scheme: dark) {
  :root{--color1:#111;--color2:#222;}
}

JavaScript can also detect the preference with window.matchMedia('(prefers-color-scheme: dark)').

Theme Service Implementation

A Theme class stores an ID, name, and a map of variable names to values. ThemeService creates or reuses a <style id="devuiThemeVariables"> element, sets its innerText to the formatted CSS variables, and updates a ui-theme attribute on body. It also notifies listeners via an IEventBus interface:

export class Theme {
  id: ThemeId;
  name: string;
  data: { [cssVarName: string]: string };
}
class ThemeService {
  applyTheme(theme: Theme) {
    // create or reuse style element
    this.contentElement.innerText = ':root { ' + this.formatCSSVariables(theme.data) + ' }';
    document.body.setAttribute('ui-theme', this.currentTheme.id);
    this.notify(theme, 'themeChanged');
  }
  formatCSSVariables(themeData) {
    return Object.keys(themeData).map(cssVar => '--' + cssVar + ':' + themeData[cssVar]).join(';');
  }
  private notify(theme, eventType) { if (this.eventBus) this.eventBus.trigger(eventType, theme); }
}
export interface IEventBus {
  on(eventName: string, callbacks: Function): void;
  off(eventName: string, callbacks: Function): void;
  trigger(eventName: string, data: any): void;
}

Image Handling in Dark Mode

Images cannot be simply inverted; instead, provide separate light and dark assets or adjust opacity. Simple approach:

body[ui-theme-mode='dark'] img { opacity: 0.8; }

More complex approach adds a semi‑transparent overlay:

body[ui-theme-mode='dark'] .dark-mode-image-overlay::before {
  content: '';
  position: absolute; inset: 0;
  background: rgba(50,50,50,0.5);
}

Theme Change Subscription for Third‑Party Components

When the theme changes, emit a themeChanged event via the event bus; third‑party components can listen and adjust their own styles accordingly.

Compatibility and Polyfills

Older browsers that lack CSS variable support can use css-vars-ponyfill to emulate variables at runtime:

import cssVars from 'css-vars-ponyfill';
cssVars({ watch: true, silent: true });

A PostCSS plugin can add fallback values for var() expressions, ensuring graceful degradation:

module.exports = postcss.plugin('postcss-plugin-add-origin-css-var-value', () => root => {
  root.walkDecls(decl => {
    if (decl.value && decl.value.match(/var\(--.*?,(.*?)\)/g)) {
      decl.cloneBefore({ value: decl.value.replace(/var\(--.*?,(.*?)\)/g, '$1') });
    }
  });
});

Challenges and Discussion

Mapping colors for dark mode is not a simple linear brightness reduction; non‑linear algorithms and HSL adjustments are explored but remain complex. Sass/Less functions like lighten or fadeout cannot operate on var() values because they become strings. One workaround is to perform color transformations before assigning them to CSS variables.

Overall, by defining semantic color variables, using runtime CSS variable updates, handling images appropriately, and providing polyfills, developers can create robust dark‑mode experiences across modern and legacy browsers.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

frontendResponsive DesignCSS VariablesDark ModeTheme Switching
Huawei Cloud Developer Alliance
Written by

Huawei Cloud Developer Alliance

The Huawei Cloud Developer Alliance creates a tech sharing platform for developers and partners, gathering Huawei Cloud product knowledge, event updates, expert talks, and more. Together we continuously innovate to build the cloud foundation of an intelligent world.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.