FrontendCSSDesignFrontendResponsiveUI

CSS Custom Properties: Build Themeable, Maintainable Design Systems

CSS custom properties, also called CSS variables, enable dynamic theming, responsive design tokens, and maintainable style systems without preprocessors.

Abdur Razzak

Abdur Razzak

Full-Stack Web Developer

May 26, 2026 8 min read

CSS custom properties, commonly called CSS variables, are entities defined by CSS authors that contain specific values to be reused throughout a document. Unlike preprocessor variables from Sass or Less that are compiled away during the build step and produce static CSS, custom properties are live in the browser and can be read and updated by JavaScript at runtime. This fundamental difference makes CSS custom properties far more powerful for dynamic theming, responsive adaptations, and component-level style overrides. They follow the normal CSS cascade and inheritance rules, meaning a custom property defined on a parent element is available to all of its descendants, and a more specific rule on a child element overrides the inherited value for that subtree. This property-cascade behavior enables the token inheritance patterns that underpin modern design systems and makes implementing dark mode or brand themes a matter of changing a small set of values at the root level.

Declaring and Using Custom Properties

Custom properties are declared with a double-hyphen prefix followed by the property name: --primary-color: #7c3aed. This declaration can appear inside any CSS selector, and the property is then available to all descendant elements of that selector. Declaring properties on the :root pseudo-class makes them globally available throughout the entire document, which is the standard practice for design tokens. To use a custom property, wrap its name in the var() function: color: var(--primary-color). The var function accepts an optional fallback value as its second argument, which is used when the custom property is not defined or is invalid: color: var(--primary-color, blue). Fallback values can themselves reference other custom properties. Custom properties are case-sensitive, so --primary-color and --Primary-Color are two distinct properties. Invalid or undefined custom properties used in var() cause the property to inherit or fall back to its initial value, depending on whether the property is inherited.

Design Tokens: Building a Consistent Visual Language

Design tokens are the named values that represent the visual decisions in a design system: colors, typography scales, spacing units, border radii, shadow levels, transition durations, and z-index layers. Storing these as CSS custom properties at the :root level creates a single source of truth for every visual constant in your application. When a designer decides to change the primary brand color from purple to teal, you update one custom property value and every component that uses that token updates automatically. This is dramatically more maintainable than doing a find-and-replace across hundreds of CSS files. Organizing tokens into semantic groups, such as --color-text-primary and --color-text-secondary rather than raw color values, creates an abstraction layer between the raw design values and their usage contexts, making it possible to change the visual language of an entire application by swapping one set of token values for another.

Dark Mode Theming with Custom Properties

CSS custom properties make implementing dark mode remarkably clean compared to duplicating entire stylesheets or maintaining separate CSS files for each theme. Define all color tokens as custom properties with light mode values on the :root selector. Then override those same properties with dark mode equivalents inside a media query that matches the prefers-color-scheme: dark user preference, or inside a data-theme equals dark attribute selector on the html element for JavaScript-toggled themes. Every component in the application automatically uses the updated token values without any changes to the component styles themselves. This approach ensures complete coverage because any component that uses design tokens picks up the theme change automatically, eliminating the risk of components that were missed during a manual find-and-replace operation. It also supports three-way light, dark, and system preference modes with minimal additional code.

Dynamic Updates with JavaScript

Because CSS custom properties live in the browser rather than being compiled away, they can be read and updated by JavaScript at runtime using the style API. To read a custom property value, call window.getComputedStyle(document.documentElement).getPropertyValue('--primary-color'). To update a custom property, call document.documentElement.style.setProperty('--primary-color', '#10b981'). These updates immediately cascade to all elements that inherit the property, causing instant visual updates throughout the page without any JavaScript re-rendering or class manipulation. This capability enables powerful interactions like color picker controls that update a theme in real time, user preference panels that persist custom color choices to localStorage, and dynamic spacing adjustments based on viewport calculations that are more complex than pure CSS can express. Updates through setProperty respect CSS specificity and only override properties at the element level, not in the stylesheet.

Component-Level Theming and Scoped Properties

Custom properties defined on a specific component class rather than on :root are scoped to that component and its children. This enables component-level theming where the same component can look different depending on context. A button component might define --button-bg and --button-text properties with default values in its own selector. A parent element can then override these properties for a specific usage context without touching the component stylesheet at all. This pattern is central to how modern design systems like Radix UI and Material Web Components expose theming APIs. Parent components declare overrides for the tokens they want to change, and child components consume those tokens through var() without needing to know where the values come from. The result is a flexible, composable theming system where visual customization is additive rather than requiring CSS specificity battles or style overrides.

Responsive Design with Custom Properties

CSS custom properties can be redefined inside media queries, allowing design token values to change responsively rather than requiring every property that uses a token to be repeated inside each breakpoint. A spacing scale token like --spacing-section might be 4rem on desktop and 2rem on mobile. Define the mobile value as the default and override it inside a min-width media query for larger screens. Every component using --spacing-section automatically adapts without needing individual media query overrides. This technique dramatically reduces the amount of repetitive media query code in large stylesheets. Combined with the clamp() and calc() CSS functions, custom properties enable fluid typography and spacing systems where values scale smoothly between minimum and maximum bounds across the full range of viewport widths, producing more refined responsive behavior than the step-function approach of traditional breakpoint-based responsive design.

Share this article

All posts
#CSS#Design#Frontend#Responsive#UI
Abdur Razzak — Full Stack Web Developer

Full-Stack Expert

MERN · React · Next.js · WordPress