What Are CSS Custom Properties?
CSS custom properties (commonly called CSS variables) are entities defined by CSS authors that represent specific values to be reused throughout a document. Unlike preprocessor variables, custom properties are true CSS values that participate in the cascade, can be accessed and modified via JavaScript, and inherit through the DOM tree.
The CSS custom properties module adds support for cascading variables as a new primitive value type that is accepted by all CSS properties. This means custom properties can be used anywhere a standard CSS value is expected, providing unprecedented flexibility in stylesheet architecture. Modern web development practices leverage these features to create maintainable design systems.
Key Differences from Preprocessor Variables
- Runtime capability: Custom properties can be changed via JavaScript at runtime
- Cascade participation: Custom properties follow standard cascade rules
- Inheritance: Custom properties inherit through the DOM tree automatically
- No build step required: Native browser support without compilation
Understanding the fundamental features that make custom properties powerful
Cascade Participation
Custom properties follow standard cascade rules including origin, specificity, and source order. The winning declaration becomes the inherited value for child elements.
Automatic Inheritance
Child elements automatically receive custom property values unless they explicitly override them, creating natural scoping without additional effort.
JavaScript Access
Read and write custom properties at runtime using getPropertyValue() and setProperty(), enabling dynamic theming and real-time adjustments.
Type Safety with @property
The @property at-rule allows explicit type declarations, syntax validation, and control over inheritance behavior for more robust implementations.
Declaring Custom Properties
Basic Syntax with Double-Dash Prefix
The most common method for declaring custom properties uses the double-dash prefix (--) followed by the property name. This syntax has been widely supported since 2015.
:root {
--primary-color: #2563eb;
--spacing-unit: 1rem;
--border-radius: 0.5rem;
}
The selector in which you declare the custom property defines its scope. Declaring on :root makes the property globally available, while declaring on a specific selector limits access to descendants.
Important: Custom property names are case-sensitive -- --my-color and --My-color are treated as completely separate properties.
Advanced Declaration with @property At-Rule
The @property at-rule provides additional control over custom properties:
@property --brand-color {
syntax: "<color>";
inherits: false;
initial-value: #3b82f6;
}
syntax: Specifies the expected value type for validationinherits: Controls whether the property inherits from parentsinitial-value: Sets the default when no declaration is found
Referencing Custom Properties with var()
Once declared, custom properties are accessed using the var() function. The function takes the property name as its first argument and an optional fallback value as the second.
.button {
background-color: var(--primary-color);
padding: var(--spacing-unit, 1rem);
border-radius: var(--border-radius);
}
Fallback Values
The fallback value is used when the referenced custom property is not defined or is invalid:
.card {
background-color: var(--card-bg, #ffffff);
color: var(--card-text, #333333);
}
Fallback values can even contain other var() calls, allowing for progressive fallback chains.
Understanding Inheritance and Scope
Automatic Inheritance Behavior
Custom properties defined using the double-dash prefix are subject to inheritance, meaning child elements automatically receive the value unless they explicitly override it. This inheritance works through the DOM tree regardless of nesting depth.
When you declare a custom property on an element, all descendant elements can access that value. A descendant that declares the same property with a new value creates a new scope boundary.
Scoping Strategies
Global Tokens: Declare design tokens on :root for global access:
:root {
--color-primary: #2563eb;
--font-size-base: 1rem;
}
Component Scoping: Declare component-specific properties on the component selector:
.card {
--card-background: #ffffff;
--card-text: #1f2937;
--card-border: #e5e7eb;
}
Hybrid Approach: Use global tokens as defaults, override at component level:
.button {
--button-background: var(--color-primary);
--button-text: var(--color-text-on-primary);
}
JavaScript Manipulation and Dynamic Theming
Custom properties bridge CSS and JavaScript, enabling sophisticated runtime customization. This capability is fundamental to implementing dynamic user experiences that respond to user preferences and behavior.
Reading and Writing Custom Properties
Custom properties can be read and modified via JavaScript at runtime:
Reading a value:
const element = document.documentElement;
const styles = getComputedStyle(element);
const primaryColor = styles.getPropertyValue('--primary-color');
Writing a value:
document.documentElement.style.setProperty('--primary-color', '#7c3aed');
Implementing Dark Mode
Dark mode implementation demonstrates the power of combining custom properties with media queries and JavaScript:
:root {
--background-primary: #ffffff;
--text-primary: #111827;
--border-color: #e5e7eb;
}
@media (prefers-color-scheme: dark) {
:root {
--background-primary: #111827;
--text-primary: #f9fafb;
--border-color: #374151;
}
}
For user-toggleable themes, JavaScript modifies the custom properties based on stored preferences, enabling instant theme switching without page reloads.
Performance Considerations
Browser Rendering Impact
Custom properties perform comparably to standard CSS values in modern browsers because they are resolved at computed value time rather than creating additional CSS rule processing overhead. The browser treats custom properties like any other value once resolved.
Performance characteristics:
- No inherent penalty for using custom properties extensively
- JavaScript manipulation triggers style recalculation for affected elements
- Setting properties on
:rootrecalculates styles for the entire document
Performance optimization through efficient CSS architecture directly impacts SEO rankings, as search engines prioritize fast-loading, well-optimized websites.
Animation Optimization
Custom properties work efficiently with CSS transitions and animations:
.button {
--hover-overlay: 0;
background: rgba(37, 99, 235, calc(1 - var(--hover-overlay)));
transition: background 0.2s ease;
}
.button:hover {
--hover-overlay: 0.1;
}
Optimization Strategies
- Prefer semantic naming to reduce total number of properties
- Use CSS-based solutions (media queries) over JavaScript for frequent changes
- Scope properties to components when possible
- Avoid overly complex calculations in
calc()with multiplevar()references
Best Practices for Maintainable CSS
Semantic Naming Conventions
Name custom properties by their semantic purpose rather than visual appearance:
:root {
/* Colors by role - descriptive and maintainable */
--color-background-page: #ffffff;
--color-background-surface: #f9fafb;
--color-text-primary: #111827;
--color-text-secondary: #6b7280;
--color-border-default: #e5e7eb;
/* Spacing by purpose */
--space-inline-content: 1rem;
--space-stack-section: 2rem;
--space-gap-component: 0.5rem;
}
Design System Token Layers
Organize custom properties into logical layers:
Foundation tokens - Raw values:
:root {
--color-blue-500: #3b82f6;
--color-blue-600: #2563eb;
}
Semantic tokens - Contextual meaning:
:root {
--color-primary: var(--color-blue-600);
--color-success: #059669;
}
Component tokens - Component-specific:
.button-primary {
--button-background: var(--color-primary);
--button-text: #ffffff;
}
Building comprehensive design systems with custom properties is a core service of our web development team, ensuring consistency across all digital touchpoints.
Common Patterns and Examples
Responsive Values with Media Queries
:root {
--container-padding: 1rem;
--font-size-base: 1rem;
}
@media (min-width: 768px) {
:root {
--container-padding: 2rem;
--font-size-base: 1.125rem;
}
}
@media (min-width: 1200px) {
:root {
--container-padding: 3rem;
}
}
Container-Queried Components
.card-grid {
--grid-columns: 1;
display: grid;
grid-template-columns: repeat(var(--grid-columns), 1fr);
gap: var(--space-gap);
}
@container (min-width: 640px) {
.card-grid {
--grid-columns: 2;
}
}
Conditional Values with calc()
.button {
--button-scale: 1;
transform: scale(var(--button-scale));
}
.button:active {
--button-scale: 0.95;
}
Frequently Asked Questions
What is the difference between CSS custom properties and preprocessor variables?
CSS custom properties are runtime values that can be modified via JavaScript and participate in the cascade. Preprocessor variables (like Sass) are compiled away at build time and cannot be changed after compilation. Custom properties also inherit through the DOM, while preprocessor variables do not.
Can I use custom properties in media queries?
No, you cannot use the var() function directly in media query definitions. However, you can declare custom properties at different breakpoints using media queries, which then affect all properties that reference those custom properties.
Do custom properties work in all modern browsers?
Yes, CSS custom properties have been supported in all modern browsers since 2018. The basic --prefix syntax works everywhere, while the @property at-rule requires newer browsers (2020+) but has good support in current versions.
How do I debug custom property issues?
Use browser DevTools to inspect custom properties. In Chrome DevTools, the Styles panel shows all custom properties and their values. You can also use getComputedStyle() in the console to read current values and test different values by editing inline styles.