What Are CSS Custom Properties?
CSS custom properties, officially called cascading variables, represent one of the most powerful additions to CSS in recent years. Unlike preprocessor variables (Sass, Less) that are compiled away at build time, CSS custom properties are native to the browser, cascade through the DOM, and can be manipulated with JavaScript in real-time.
Key characteristics that make custom properties unique:
- Native to the browser: Exist in the CSSOM (CSS Object Model)
- Cascade like standard properties: Follow inheritance and specificity rules
- Runtime manipulation: Can be modified with JavaScript in real-time
- DOM inheritance: Values flow naturally through the element hierarchy
- Media query support: Enable responsive theming without JavaScript
Unlike preprocessor variables which are text substitutions performed during compilation, CSS custom properties are actual CSS values that browsers can track, optimize, and modify dynamically. This fundamental distinction means CSS variables can respond to user preferences, adapt to different contexts, and enable features like dark mode and accessibility themes without requiring a rebuild or JavaScript intervention.
The "cascading" nature of these properties is what sets them apart from their preprocessor counterparts. When you define a custom property on a parent element, all children inherit that value by default. But unlike traditional CSS inheritance, you can override these values at any level--on specific components, within media queries, or through inline styles. This creates a powerful, predictable system for managing design tokens across large applications.
For modern web development projects, CSS custom properties have become essential infrastructure, replacing preprocessor variables while enabling capabilities that were previously impossible with static CSS alone.
1:root {2 --primary-color: #0052CC;3 --spacing-unit: 1rem;4 --font-size-base: 16px;5 --border-radius: 4px;6}7 8.element {9 --local-variable: "scoped value";10 padding: var(--spacing-unit);11 font-size: var(--font-size-base);12}Syntax: Declaring and Using Custom Properties
Declaration Syntax
Custom properties are declared using two dashes (--) as a prefix for the property name, followed by any valid CSS value:
:root {
--primary-color: #0052CC;
--spacing-unit: 1rem;
--font-size-base: 16px;
--border-radius: 4px;
}
Naming Conventions
Effective naming conventions are crucial for maintainability:
- Use kebab-case consistently:
--primary-colornot--primaryColor - Group by category:
--color-,--spacing-,--font- - Use semantic names:
--color-action-primaryrather than--blue-500 - Avoid generic names: Prevent naming collisions with descriptive prefixes
Following established design token conventions helps teams maintain consistency across large codebases. Semantic naming allows you to change a color's value once and have it update everywhere, while descriptive prefixes prevent conflicts when integrating third-party components or design systems. For teams building comprehensive design systems, establishing naming conventions early prevents technical debt as projects scale.
Accessing Values with var()
Values are accessed using the var() function anywhere a CSS value is expected:
.button {
background-color: var(--primary-color);
padding: var(--spacing-unit);
font-size: var(--font-size-base);
}
Fallback Values
Provide fallback values when a custom property might not be defined:
.card {
background-color: var(--card-bg, #ffffff);
border: 1px solid var(--border-color, #e0e0e0);
}
Multiple fallbacks can be chained, and fallbacks can even contain other var() calls. This pattern is especially useful when creating reusable components that should work with or without a design system:
.button {
background-color: var(
--button-bg,
var(--color-primary, #0052CC)
);
}
The chained fallback pattern ensures your component has a sensible default even when neither component-level nor global tokens are defined, making components more portable across projects.
Understanding how custom properties flow through your stylesheets
Global Variables with :root
The :root pseudo-class is the ideal place for global custom properties, ensuring they're available throughout the entire document.
Component-Scoped Variables
Prevent naming collisions by defining variables at the component level, keeping styles encapsulated and maintainable.
Natural Inheritance
Custom properties inherit by default, allowing child elements to access and override parent-defined values.
Cascade Behavior
Like standard CSS properties, custom properties follow specificity and origin rules, enabling powerful override patterns.
Scope and Inheritance
Global Variables with :root
The :root pseudo-class represents the highest-level element in the DOM (typically <html>), making it the ideal place for global custom properties:
:root {
--brand-color: #3498db;
--text-color: #2c3e50;
--background-color: #ffffff;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--spacing-lg: 2rem;
}
Using :root ensures variables are available throughout the entire document while following standard CSS specificity rules.
Component-Scoped Variables
Component-scoped variables prevent naming collisions and reduce CSS bundle size:
.card {
--card-padding: 1.5rem;
--card-border-radius: 8px;
--card-shadow: 0 2px 4px rgba(0,0,0,0.1);
padding: var(--card-padding);
border-radius: var(--card-border-radius);
box-shadow: var(--card-shadow);
}
This approach mirrors modern component-based architecture and keeps styles encapsulated, as noted by LogRocket's analysis of CSS variable patterns.
Inheritance Behavior
Custom properties inherit by default, meaning child elements can access and override parent-defined variables:
.parent {
--text-color: #000000;
}
.child {
color: var(--text-color);
}
.child-of-child {
/* Override for this subtree */
--text-color: #333333;
}
This inheritance enables powerful contextual theming patterns. For example, you might define a base text color globally, then override it within a dark section without affecting the rest of the page:
:root {
--text-primary: #1a1a1a;
}
.dark-section {
--text-primary: #ffffff;
}
/* All children of .dark-section automatically use white text */
.dark-section h1,
.dark-section p,
.dark-section a {
color: var(--text-primary);
}
This inheritance model also enables advanced use cases like user stylesheets and accessibility preferences, where end users can define their own custom properties that cascade into your design. Implementing proper inheritance patterns through CSS custom properties creates more flexible and maintainable stylesheets for responsive web applications.
1:root {2 --bg-primary: #ffffff;3 --text-primary: #1a1a1a;4 --text-secondary: #666666;5 --accent-color: #3498db;6}7 8[data-theme="dark"] {9 --bg-primary: #1a1a1a;10 --text-primary: #ffffff;11 --text-secondary: #a0a0a0;12 --accent-color: #5dade2;13}14 15[data-theme="high-contrast"] {16 --bg-primary: #000000;17 --text-primary: #ffff00;18 --text-secondary: #00ff00;19 --accent-color: #ff00ff;20}21 22body {23 background-color: var(--bg-primary);24 color: var(--text-primary);25}26 27/* No JavaScript required for basic theming! */Advanced Usage Patterns
Dynamic Theming
CSS variables excel at implementing theme systems because they can be swapped at different scopes:
:root {
--bg-primary: #ffffff;
--text-primary: #1a1a1a;
}
[data-theme="dark"] {
--bg-primary: #1a1a1a;
--text-primary: #ffffff;
}
This approach requires no JavaScript for basic theming and updates instantly when attribute values change. Users can toggle themes by simply changing a data attribute on the HTML element.
Responsive Values with Media Queries
Custom properties can be redefined within media queries for responsive design:
:root {
--font-size-body: 1rem;
--container-width: 1200px;
--grid-columns: 4;
}
@media (max-width: 1024px) {
:root {
--font-size-body: 0.9375rem;
--container-width: 960px;
--grid-columns: 3;
}
}
@media (max-width: 768px) {
:root {
--font-size-body: 0.875rem;
--container-width: 100%;
--grid-columns: 2;
}
}
This approach centralizes responsive decisions and makes maintenance significantly easier, as shown in Penpot's design token guide. For projects requiring comprehensive responsive design strategies, CSS variables provide an elegant solution for managing breakpoints and responsive values across your entire stylesheet.
Using @property for Type Safety
The @property at-rule (part of CSS Houdini) allows explicit type definitions:
@property --color-primary {
syntax: '<color>';
inherits: false;
initial-value: #000000;
}
@property --spacing-unit {
syntax: '<length>';
inherits: true;
initial-value: 1rem;
}
This provides:
- Type validation during CSS parsing - invalid values will cause a console warning
- Better developer tooling support - IDEs can provide autocomplete and type hints
- Explicit inheritance control - decide whether properties cascade or stay scoped
- Animation support for custom properties - enable smooth transitions between values
Browser support for @property has improved significantly, with all modern browsers now supporting this feature. For projects requiring broader compatibility, the initial-value provides a fallback for unsupported browsers. The type safety provided by @property makes it particularly valuable for design systems where incorrect value types can cause subtle visual bugs that are difficult to track down.
1// Reading custom property values2const element = document.documentElement;3const styles = getComputedStyle(element);4const primaryColor = styles.getPropertyValue('--primary-color');5 6// Setting custom property values7document.documentElement.style.setProperty('--primary-color', '#e74c3c');8 9// Real-world example: System theme detection10const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');11 12// Apply theme based on system preference13document.documentElement.setAttribute('data-theme', prefersDark ? 'dark' : 'light');14 15// Listen for preference changes16prefersDark.addEventListener('change', (e) => {17 document.documentElement.setAttribute('data-theme', e.matches ? 'dark' : 'light');18});JavaScript Integration
Reading Custom Property Values
JavaScript can read custom property values using the computed style:
const element = document.documentElement;
const styles = getComputedStyle(element);
const primaryColor = styles.getPropertyValue('--primary-color');
console.log(primaryColor); // e.g., "rgb(0, 82, 204)"
Reading from specific elements works similarly, following CSS inheritance rules. When you read a property from a child element, it will return the inherited or overridden value rather than the original declaration.
Setting Custom Property Values
JavaScript can modify custom properties dynamically:
// Set a new value
document.documentElement.style.setProperty('--primary-color', '#e74c3c');
// Get current value
const oldValue = document.documentElement.style.getPropertyValue('--primary-color');
// Remove a custom property
document.documentElement.style.removeProperty('--temporary-variable');
Real-World JavaScript Patterns
Practical applications include:
- User preference storage: Save theme choices to localStorage and apply on page load
- Dynamic theming: Apply themes based on time of day or user location
- Accessibility features: Adjust contrast for users with visual impairments
- A/B testing: Dynamically change design variations for experimentation
- Brand customization: Allow enterprise customers to apply their brand colors without code changes
Error handling is important when working with CSS custom properties. Always check if a property exists before attempting to read it, and consider providing defaults when reading values that may not be set:
function getCustomProperty(element, propertyName) {
const styles = getComputedStyle(element);
return styles.getPropertyValue(propertyName) || null;
}
// Safe reading with fallback
const value = getCustomProperty(document.documentElement, '--brand-color')
|| '#0052CC'; // Default brand color
As documented by MDN Web Docs, the JavaScript API provides full access to the CSSOM, enabling sophisticated integrations between JavaScript application state and CSS styling. For teams building interactive web applications, mastering JavaScript integration with CSS custom properties opens up powerful possibilities for dynamic user experiences.
Performance Benefits
0
JavaScript compilation required
1
Source of truth for all platforms
Instant
Runtime updates
Native
Browser optimization
Performance Considerations
Why CSS Variables Outperform Preprocessor Variables
CSS custom properties provide performance advantages over preprocessor variables:
- Runtime modification: Changes apply instantly without recompilation
- Single source: No CSS duplication when values need updates
- Smaller bundles: No preprocessor runtime or duplicated code
- Native browser optimization: Browsers can optimize custom property access through the CSSOM
Performance Best Practices
While CSS variables are performant, certain patterns maximize efficiency:
- Define global variables in
:rootto avoid repeated declarations - Use semantic naming to reduce the need for changes
- Group related variables to minimize cascade recalculations
- Avoid excessive nesting of
var()calls which can complicate browser resolution - Use
@propertyfor type safety when animation is needed
Modern browsers have optimized CSS custom property handling extensively. The CSSOM (CSS Object Model) allows browsers to track custom property dependencies, updating only the elements affected when a property changes. This is significantly more efficient than preprocessor variables, which are simply text-replaced during compilation and result in repeated values throughout the generated CSS.
Regarding bundle size, LogRocket's performance analysis shows that CSS variables typically result in smaller stylesheets because values are declared once and referenced multiple times. This reduces not only file size but also the memory footprint of stylesheets in the browser. Additionally, because custom properties can be changed at runtime, you may eliminate the need for multiple theme stylesheets, further reducing overall bundle size.
For very large applications, consider lazy-loading theme modules--load only the CSS variables needed for the current view, and load additional themes on demand. This pattern is especially useful for applications with multiple product brands or extensive theming options, contributing to overall website performance optimization.
1/* Primitive tokens - raw design values */2:root {3 --color-blue-400: #3498db;4 --color-blue-500: #2980b9;5 --color-blue-600: #2574a9;6 7 --spacing-1: 0.25rem;8 --spacing-2: 0.5rem;9 --spacing-4: 1rem;10 --spacing-8: 2rem;11}12 13/* Semantic tokens - contextual usage */14:root {15 --color-primary: var(--color-blue-500);16 --color-primary-hover: var(--color-blue-400);17 --color-primary-active: var(--color-blue-600);18 19 --space-xs: var(--spacing-1);20 --space-sm: var(--spacing-2);21 --space-md: var(--spacing-4);22 --space-lg: var(--spacing-8);23}24 25/* Component tokens - specific component usage */26:root {27 --button-padding: var(--space-sm) var(--space-md);28 --button-radius: var(--space-xs);29 --button-font-weight: 600;30}Best Practices and Naming Conventions
Naming Patterns
Effective naming conventions create maintainable systems:
/* Primitive tokens - raw design values */
--color-blue-500: #3498db;
--color-blue-600: #2980b9;
/* Semantic tokens - contextual usage */
--color-primary: var(--color-blue-500);
--color-primary-hover: var(--color-blue-600);
/* Component tokens - specific component usage */
--button-background: var(--color-primary);
--button-text: var(--color-white);
This hierarchy separates raw values from usage, enabling easy theme updates and design system evolution.
Organization Strategies
For design systems and large projects:
/* tokens-colors.css - Color tokens only */
:root {
--color-brand-primary: #3498db;
--color-brand-secondary: #2ecc71;
/* ... other colors */
}
/* tokens-typography.css - Typography tokens */
:root {
--font-family-heading: 'Inter', sans-serif;
--font-size-h1: 2.5rem;
/* ... other typography */
}
/* tokens-spacing.css - Spacing tokens */
:root {
--space-unit: 0.25rem;
/* ... other spacing */
}
This modular approach enables team collaboration and partial updates.
Design Token Standards
The W3C Design Tokens Community Group (DTCG) has established specifications for design token interchange. Following these standards ensures your CSS custom properties can integrate with design tools and cross-platform implementations:
- Use predictable naming: Group tokens by category, then by purpose
- Define type when possible: Use
@propertyfor type safety - Document token purposes: Use CSS comments to explain usage context
- Version your token system: Track changes to prevent breaking updates
Adopting the W3C DTCG format means your design tokens can be transformed automatically from design tools like Figma or Penpot into CSS custom properties, ensuring consistency between design and development implementations. This workflow is particularly valuable for teams maintaining scalable design systems at scale.
Frequently Asked Questions
Summary and Key Takeaways
CSS custom properties (cascading variables) have transformed how developers approach CSS architecture. Their ability to cascade, inherit, and be manipulated at runtime opens possibilities that preprocessor variables simply cannot match.
Key takeaways:
- Use
--prefix for declarations andvar()for access - Define global values in
:root, component values in component selectors - Leverage inheritance for contextual theming without modifying individual elements
- Use
@propertyfor type safety and animation support in complex projects - Integrate with JavaScript for dynamic, real-time updates
- Adopt semantic naming conventions for maintainable design systems
- Separate primitive from semantic tokens for flexible theming
Mastering CSS variables is essential for modern web development, enabling more maintainable, performant, and dynamic stylesheets that adapt to user preferences and application state.
Related Topics: