Using CSS Custom Properties

Master CSS variables for dynamic, maintainable styling that adapts at runtime. Build themeable design systems with native browser features.

What Are CSS Custom Properties?

CSS Custom Properties, commonly referred to as CSS variables, represent a fundamental shift in how we approach styling on the web. Unlike preprocessor variables (Sass, Less) that are compiled away at build time, CSS custom properties are native browser features that exist at runtime. This means they can be read, modified, and responded to dynamically through JavaScript, making them far more powerful than their preprocessor counterparts.

At Digital Thrive, we leverage CSS custom properties extensively in our web development projects to create maintainable, themeable design systems. The ability to change an entire application's color scheme by modifying a single value provides immense value for clients who need brand flexibility without sacrificing performance. Our approach integrates seamlessly with modern SEO best practices by ensuring fast, responsive user experiences.

Custom properties allow you to store values that can be reused throughout your stylesheets. They follow the cascade, inherit values from parents, and can be modified with JavaScript in real-time. This combination of features makes them ideal for implementing dark mode toggles, responsive design systems, and component-level theming that adapts to different contexts within a page. For clients requiring intelligent automation solutions, our AI automation services can integrate dynamic styling with smart user interactions.

Key capabilities include:

  • Runtime access: Values can be read and modified with JavaScript in real-time
  • Cascade awareness: Properties follow CSS cascade and inheritance rules
  • Dynamic theming: Change entire application themes by updating a few values
  • Component isolation: Scope variables to specific components without leakage

Declaring Custom Properties

Basic Syntax

CSS custom properties are declared using two dashes as a prefix (--) followed by an identifier of your choice. The value can be any valid CSS value, including colors, lengths, times, or even complex expressions. Declaration occurs within a ruleset, and the selector determines where the custom property is accessible.

/* Define a custom property on an element */
section {
 --main-bg-color: brown;
}

/* Define globally using :root */
:root {
 --primary-color: #0066ff;
 --spacing-unit: 1rem;
 --font-base: 16px;
}

The selector determines the scope. Properties declared on :root are globally available throughout the document, while properties declared on more specific selectors remain isolated to that scope and its descendants. This scoping behavior is crucial for building modular, component-based stylesheets that don't leak styles unintentionally.

The @property At-Rule

For scenarios requiring more control, CSS provides the @property at-rule, which allows you to define custom properties with explicit type checking, default values, and inheritance control. This feature is particularly valuable when building design systems where type safety prevents runtime errors.

/* Define a typed custom property */
@property --logo-color {
 syntax: "<color>";
 inherits: false;
 initial-value: #c0ffee;
}

@property --spacing-scale {
 syntax: "<number>";
 inherits: true;
 initial-value: 1;
}

When you use @property, the browser understands the expected type of the value, which enables better developer experience through tooling support and prevents invalid values from being assigned. The inherits property controls whether child elements receive the value from their parent or use the initial value instead.

Naming Conventions

Clear naming conventions make CSS custom properties maintainable at scale. We recommend organizing variables by category and using semantic names that describe purpose rather than appearance.

:root {
 /* By category - colors */
 --color-primary: #0066ff;
 --color-secondary: #6366f1;
 --color-success: #10b981;
 --color-warning: #f59e0b;
 --color-danger: #ef4444;

 /* By scale - creating a gray scale */
 --gray-50: #f9fafb;
 --gray-100: #f3f4f6;
 --gray-200: #e5e7eb;
 --gray-500: #6b7280;
 --gray-900: #111827;

 /* By usage - semantic names */
 --text-primary: var(--gray-900);
 --text-secondary: var(--gray-500);
 --bg-page: var(--gray-50);
 --border-color: var(--gray-200);
}

Custom property names are case-sensitive, meaning --my-color and --My-color are treated as separate properties.

Using the var() Function

Basic Usage

Once a custom property is defined, you access its value using the var() function. This function can be used anywhere a CSS value is expected, including property values, shorthand properties, and even within other CSS functions.

:root {
 --primary: #0066ff;
 --padding: 1rem;
}

.button {
 background: var(--primary);
 padding: var(--padding);
 color: white;
 border: 2px solid var(--primary);
 box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}

The var() function accepts a custom property name as its first argument and optionally a fallback value as the second argument. The fallback is used when the custom property is not defined, providing graceful degradation.

Variables in Complex Values

Custom properties work seamlessly with CSS color functions, transform functions, and mathematical calculations. This capability enables powerful dynamic styling patterns that would require significant JavaScript without custom properties.

:root {
 --hue: 210;
 --saturation: 100%;
 --lightness: 50%;
}

.element {
 color: hsl(var(--hue), var(--saturation), var(--lightness));
 background: rgba(var(--r), var(--g), var(--b), 0.5);
 transform: rotate(var(--rotate, 0deg)) scale(var(--scale, 1));
}

When used with calc(), custom properties enable responsive calculations that adapt to changing base values. This approach is particularly effective for fluid typography and spacing systems that need to scale across viewport sizes.

Scope and Inheritance

How Inheritance Works

CSS custom properties follow standard CSS inheritance rules. When a custom property is declared on an element, all descendant elements can access that value unless they explicitly override it. This inheritance model enables powerful component-scoped styling patterns.

:root {
 --color: blue;
}

.parent {
 --color: red;
}

.parent .child {
 color: var(--color); /* red - inherits from parent */
}

.child .grandchild {
 --color: green;
 color: var(--color); /* green - uses local declaration */
}

Understanding inheritance is crucial for debugging custom property issues. The cascade determines which value applies, considering specificity, source order, and custom property declarations along the ancestor chain.

Component Scoping

Component-scoped custom properties create self-contained styles that don't interfere with other parts of the application. Each component defines its own variables, and child components inherit or override them intentionally.

.card {
 --card-padding: 1.5rem;
 --card-bg: white;
 --card-border: 1px solid #e5e5e5;
 --card-radius: 8px;

 padding: var(--card-padding);
 background: var(--card-bg);
 border: var(--card-border);
 border-radius: var(--card-radius);
}

.card.large {
 --card-padding: 2.5rem;
 --card-radius: 12px;
}

.card.dark {
 --card-bg: #1a1a1a;
 --card-border: 1px solid #333;
}

This pattern enables variant-based styling without duplicating code. The modifier classes (.large, .dark) simply override the relevant variables, and the component's core styles automatically adapt.

Preventing Unwanted Inheritance

In some cases, you may want to prevent inheritance entirely. The initial keyword resets a custom property to its initial value, which is either the browser default or the value specified in an @property definition.

.isolated {
 --color: initial;
 color: var(--color); /* Uses browser default */
}

Fallback Values

The var() function accepts a second argument that serves as a fallback when the custom property is not defined. Fallbacks can be simple values or even include other var() calls, creating a chain of alternatives.

.element {
 /* Simple fallback */
 color: var(--primary, #0066ff);
 background: var(--bg-color, #ffffff);
 padding: var(--spacing, 1rem);

 /* Chain of fallbacks - try in order */
 color: var(
 --color-theme,
 var(--color-brand, #333)
 );
}

Fallback values are particularly important for progressive enhancement, ensuring styles remain functional even when custom properties are not supported or when a specific property hasn't been defined in a particular context.

Complex Fallbacks

Fallback values can include complex CSS values, including functions, gradients, and entire property declarations. This flexibility enables sophisticated fallback strategies for different browser capabilities.

.element {
 padding: var(--padding, 1rem 2rem);
 border: var(--border, 2px solid #ccc);
 background: var(
 --gradient,
 linear-gradient(135deg, #667eea 0%, #764ba2 100%)
 );
}

When providing fallbacks for properties that don't inherit by default (such as padding or border), ensure the fallback is a complete, valid value for that property.

Theming with Custom Properties

Dark Mode Implementation

CSS custom properties excel at implementing dark mode because changing a theme requires only updating variable values. The entire interface adapts automatically without targeting individual components. Our web development team leverages these patterns to create seamless user experiences across all devices and preferences.

/* Light theme (default) */
:root {
 --bg-primary: #ffffff;
 --bg-secondary: #f5f5f5;
 --text-primary: #1a1a1a;
 --text-secondary: #666666;
 --border-color: #e5e5e5;
}

/* Dark theme */
[data-theme="dark"],
.dark-mode {
 --bg-primary: #1a1a1a;
 --bg-secondary: #2d2d2d;
 --text-primary: #ffffff;
 --text-secondary: #a0a0a0;
 --border-color: #404040;
}

/* System preference detection */
@media (prefers-color-scheme: dark) {
 :root {
 --bg-primary: #1a1a1a;
 --bg-secondary: #2d2d2d;
 --text-primary: #ffffff;
 --text-secondary: #a0a0a0;
 --border-color: #404040;
 }
}

This approach separates theme values from component styles, making it easy to add new themes or modify existing ones without touching any component code. For applications requiring intelligent theming tied to user behavior, explore our AI-powered automation solutions.

Multiple Brand Themes

For applications serving multiple brands or clients, custom properties enable runtime theme switching without code changes. Each brand simply provides its own variable values.

/* Base theme */
:root {
 --primary: #0066ff;
 --secondary: #6366f1;
}

/* Ocean brand */
[data-theme="ocean"] {
 --primary: #0ea5e9;
 --secondary: #06b6d4;
 --accent: #14b8a6;
}

/* Forest brand */
[data-theme="forest"] {
 --primary: #10b981;
 --secondary: #059669;
 --accent: #84cc16;
}

/* Sunset brand */
[data-theme="sunset"] {
 --primary: #f59e0b;
 --secondary: #ef4444;
 --accent: #ec4899;
}

Component styles remain unchanged, using var(--primary) and other semantic variable names that resolve to the appropriate brand color.

JavaScript Manipulation

Reading Custom Property Values

JavaScript can read custom property values using getComputedStyle() and getPropertyValue(). This enables dynamic styling based on user interaction, analytics data, or application state.

// Read from root element
const root = document.documentElement;
const primaryColor = getComputedStyle(root)
 .getPropertyValue('--primary-color');

console.log(primaryColor); // "#0066ff"

// Read from specific element
const card = document.querySelector('.card');
const cardPadding = getComputedStyle(card)
 .getPropertyValue('--card-padding');

// Clean up whitespace
const spacing = getComputedStyle(root)
 .getPropertyValue('--spacing')
 .trim();

The getComputedStyle() method returns the computed value, which may differ from the declared value if inheritance or cascade effects are in play.

Setting Custom Property Values

JavaScript can modify custom properties in real-time using setProperty(). Changes are immediately reflected throughout the document.

// Set on root element
document.documentElement.style
 .setProperty('--primary-color', '#ff0066');

// Set on specific element
const element = document.querySelector('.card');
element.style.setProperty('--card-bg', '#f5f5f5');

// Set multiple variables for a theme
const setTheme = (theme) => {
 const root = document.documentElement;
 root.style.setProperty('--primary', theme.primary);
 root.style.setProperty('--secondary', theme.secondary);
 root.style.setProperty('--bg', theme.background);
};

const darkTheme = {
 primary: '#4d94ff',
 secondary: '#7c3aed',
 background: '#1a1a1a'
};

setTheme(darkTheme);

This capability enables interactive features like color pickers, theme toggles, and responsive adjustments based on user preferences.

Interactive Examples

Common interactive patterns include color pickers, range sliders for sizing, and scroll-based effects.

// Color picker
const colorPicker = document.querySelector('#colorPicker');
colorPicker.addEventListener('input', (e) => {
 document.documentElement.style
 .setProperty('--primary-color', e.target.value);
});

// Theme toggle
const toggleTheme = () => {
 const root = document.documentElement;
 const isDark = root.getAttribute('data-theme') === 'dark';
 root.setAttribute('data-theme', isDark ? 'light' : 'dark');
};

// Scroll-based variable
window.addEventListener('scroll', () => {
 const scrollPercent = window.scrollY /
 (document.body.scrollHeight - window.innerHeight);
 document.documentElement.style
 .setProperty('--scroll-progress', scrollPercent);
});

These patterns are particularly effective for creating engaging user experiences without the performance cost of manipulating individual element styles.

Calculations with calc()

Math Operations

CSS custom properties combine powerfully with calc() to create dynamic, responsive values. You can perform arithmetic operations on custom property values to derive new measurements.

:root {
 --base-spacing: 1rem;
 --multiplier: 2;
}

.element {
 /* Multiplication */
 margin: calc(var(--base-spacing) * 2);
 padding: calc(var(--base-spacing) * var(--multiplier));

 /* Addition and subtraction */
 width: calc(100% - var(--sidebar-width));
 height: calc(100vh - var(--header-height) - var(--footer-height));

 /* Division */
 font-size: calc(var(--base-font-size) / 1.5);
}

This approach eliminates magic numbers from stylesheets and creates relationships between values that update together.

Fluid Typography and Spacing

Combining calc() with clamp() and custom properties creates fluid designs that adapt smoothly across viewport sizes.

:root {
 --min-size: 1rem;
 --max-size: 3rem;
 --preferred-size: 2.5vw;
}

.heading {
 font-size: clamp(
 var(--min-size),
 var(--preferred-size),
 var(--max-size)
 );
}

/* Responsive spacing */
@media (min-width: 768px) {
 :root {
 --spacing-scale: 1.5;
 }
}

.element {
 padding: calc(var(--spacing-unit) * var(--spacing-scale));
}

This pattern eliminates the need for multiple breakpoints with manual value adjustments, instead creating continuous, proportional scaling. Implementing these responsive techniques improves your site's SEO performance by delivering better mobile experiences.

Color Manipulation

Custom properties enable color variations without pre-defined palettes. By breaking colors into HSL or RGB components, you can programmatically adjust lightness, saturation, or opacity.

:root {
 --hue: 210;
 --saturation: 100%;
 --lightness: 50%;
}

.element {
 background: hsl(var(--hue), var(--saturation), var(--lightness));
 border-color: hsl(
 var(--hue),
 var(--saturation),
 calc(var(--lightness) + 20%)
 );
 color: hsl(
 var(--hue),
 var(--saturation),
 calc(var(--lightness) - 30%)
 );
}

/* RGB with alpha */
:root {
 --r: 0;
 --g: 102;
 --b: 255;
}

.transparent {
 background: rgba(var(--r), var(--g), var(--b), 0.5);
}

This technique is invaluable for generating hover states, focus states, and disabled states programmatically.

Performance and Best Practices

Performance Considerations

CSS custom properties are highly performant for theme switching and dynamic styling. Changing a single custom property value updates all elements referencing it simultaneously, which is more efficient than targeting each element individually.

/* Good: Change one variable */
.theme-switcher {
 --primary: #0066ff;
}

.theme-switcher.dark {
 --primary: #4d94ff;
}

/* All elements using --primary update automatically */
.button { background: var(--primary); }
.link { color: var(--primary); }
.border { border-color: var(--primary); }

Scoped custom properties reduce cascade complexity, improving browser performance and making stylesheets easier to maintain. This performance efficiency directly contributes to better search engine rankings as page load and rendering times improve.

Organization Best Practices

Organize custom properties in a hierarchical manner, starting with base tokens (raw values), moving to semantic tokens (purpose-based references), and ending with component tokens (specific to individual components).

/* 1. Base tokens (raw values) */
:root {
 --blue-50: #eff6ff;
 --blue-500: #3b82f6;
 --blue-900: #1e3a8a;
 --space-1: 0.25rem;
 --space-4: 1rem;
}

/* 2. Semantic tokens (reference base) */
:root {
 --color-primary: var(--blue-500);
 --color-text: var(--blue-900);
 --spacing-default: var(--space-4);
}

/* 3. Component tokens (reference semantic) */
.button {
 --btn-bg: var(--color-primary);
 --btn-padding: var(--spacing-default);
}

This layered approach enables global theme changes at the semantic level while maintaining design consistency.

Common Pitfalls

Several common mistakes can cause issues when working with custom properties.

/* Don't use quotes around values */
:root {
 --spacing: "1rem"; /* Wrong - treated as string */
 --spacing: 1rem; /* Correct - numeric value */
}

/* Don't concatenate strings in var() */
.element {
 background: url(var(--base-url) + "photo.jpg"); /* Wrong */
}

/* Include full value in variable */
:root {
 --bg-image: url("/images/photo.jpg");
}
.element {
 background: var(--bg-image); /* Correct */
}

/* Always provide fallbacks for critical properties */
.element {
 color: var(--text-color, #333); /* Safe */
}

Browser Compatibility

CSS custom properties are supported in all modern browsers including Chrome, Firefox, Safari, and Edge. Internet Explorer does not support custom properties, requiring fallback strategies for legacy browser support.

/* Graceful degradation for older browsers */
.element {
 color: #0066ff; /* Static fallback */
 color: var(--primary-color); /* Modern browsers */
}

/* Or use @supports */
.element {
 color: #0066ff;
}

@supports (--css: variables) {
 .element {
 color: var(--primary-color);
 }
}

For most modern web applications targeting contemporary browsers, progressive enhancement with static fallbacks provides adequate support without complexity.

Key Benefits of CSS Custom Properties

Why modern web development relies on CSS variables

Runtime Dynamic Styling

Modify values in real-time with JavaScript. No rebuild required for theme changes or responsive adjustments.

True Cascade Support

Variables inherit and cascade naturally. Override at any level for component-level customization.

Maintainable Design Systems

Single source of truth for design tokens. Change once, update everywhere consistently.

Performance Optimized

Browser-efficient updates. Change one variable, all referencing elements update instantly.

Frequently Asked Questions

Build Dynamic, Themeable Web Applications

Our team specializes in modern CSS architecture using custom properties, design systems, and performance-optimized styling solutions for your [web projects](/services/web-development/).

Sources

  1. MDN Web Docs - Using CSS Custom Properties - Authoritative source for syntax, usage, and best practices
  2. Design.dev - CSS Variables Complete Guide - Comprehensive coverage of advanced patterns, theming, and performance