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.
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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
