What Are CSS Custom Properties?
CSS custom properties, often referred to as CSS variables, are entities defined by developers that contain specific values to be reused throughout a stylesheet. They are denoted by a -- prefix and are accessed using the var() function. Unlike preprocessor variables (such as those in Sass or Less), CSS custom properties are dynamic and can be manipulated at runtime—they respect the cascade, can be inherited, and can be updated via JavaScript or media queries.
/* Declaring a custom property */
:root {
--primary-color: #2563eb;
--font-size-base: 1rem;
--spacing-unit: 8px;
}
/* Using a custom property */
.button {
background-color: var(--primary-color);
font-size: var(--font-size-base);
padding: calc(var(--spacing-unit) * 2) calc(var(--spacing-unit) * 4);
}
Why CSS Custom Properties Matter
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Before custom properties, managing consistent values across large stylesheets was tedious. You either repeated the same hex code dozens of times or relied on preprocessors that compile away variables into static values. CSS custom properties change the game because they are live—they remain accessible in the browser and can respond to changes in context. This unlocks powerful capabilities:
- True dynamic theming: Swap entire color palettes by changing a handful of variables at runtime.
- Context-aware styling: A component can adapt its appearance based on where it lives in the DOM, thanks to inheritance.
- Responsive design without repetition: Define spacing scales or font sizes as variables and override them in media queries rather than redeclaring properties on dozens of selectors.
- JavaScript interoperability: Read and write custom properties directly via
element.style.setProperty()andgetPropertyValue(). - Cleaner, more maintainable code: Centralize your design tokens and propagate changes instantly throughout your stylesheet.
Declaring and Using Custom Properties
Syntax and Scope
A custom property is declared with two leading dashes followed by a name. The declaration must be inside a CSS rule, and the property's scope is determined by the selector. Properties declared on :root are global; properties declared on a specific class or element are scoped to that element and its descendants.
/* Global scope */
:root {
--global-max-width: 1200px;
}
/* Scoped to .card and its children */
.card {
--card-padding: 24px;
padding: var(--card-padding);
}
.card__content {
/* This inherits --card-padding from .card */
padding: calc(var(--card-padding) / 2);
}
The var() Function in Detail
The var() function accepts two arguments: the custom property name and an optional fallback value. The fallback is used when the referenced custom property is not defined or has an invalid value for the context. Crucially, the fallback is evaluated only when needed, so you can safely use other variables inside the fallback.
/* Basic usage */
.element {
color: var(--text-color, #333333);
}
/* Fallback with another variable */
.element {
color: var(--text-color, var(--secondary-text, #666));
}
/* Complex fallback with comma-separated values */
.element {
font-family: var(--custom-font, "Helvetica Neue", Arial, sans-serif);
}
/* Invalid fallback patterns — avoid these */
/* ❌ This doesn't work as expected */
.element {
padding: var(--my-padding, 10px, 20px); /* Only the first value after the name is the fallback */
}
Inheritance and the Cascade
Custom properties participate fully in the CSS cascade. They inherit by default from parent to child, just like font-size or color. This means you can set a variable on a parent element and all descendants will see that value—unless they explicitly override it. This is the foundation of component-level theming.
/* Inheritance example */
.page {
--accent-color: coral;
}
.page .sidebar {
--accent-color: teal; /* Override for sidebar and its children */
}
.page .sidebar .widget {
/* This widget gets --accent-color: teal */
border-left: 4px solid var(--accent-color);
}
.page .main-content {
/* This still gets --accent-color: coral */
background-color: var(--accent-color, transparent);
}
Practical Use Cases and Code Examples
Building a Dynamic Theme System
One of the most powerful applications of custom properties is theming. By centralizing your design tokens as variables, you can create light and dark themes—or even user-customizable themes—with minimal effort.
:root {
/* Light theme (default) */
--color-surface: #ffffff;
--color-on-surface: #1a1a1a;
--color-primary: #3b82f6;
--color-border: #e5e7eb;
--shadow-card: 0 1px 3px rgba(0, 0, 0, 0.1);
}
[data-theme="dark"] {
--color-surface: #1e293b;
--color-on-surface: #f1f5f9;
--color-primary: #60a5fa;
--color-border: #334155;
--shadow-card: 0 1px 3px rgba(0, 0, 0, 0.4);
}
/* Apply tokens throughout your styles */
body {
background-color: var(--color-surface);
color: var(--color-on-surface);
}
.card {
background-color: var(--color-surface);
border: 1px solid var(--color-border);
box-shadow: var(--shadow-card);
}
.button-primary {
background-color: var(--color-primary);
color: var(--color-surface);
}
Responsive Spacing Scales
Instead of overwriting margins and paddings on every component inside a media query, define a spacing unit variable and adjust it once. This creates a consistent rhythm across your entire layout.
:root {
--space-xs: 4px;
--space-sm: 8px;
--space-md: 16px;
--space-lg: 32px;
--space-xl: 64px;
}
/* Adjust the scale for smaller screens */
@media (max-width: 768px) {
:root {
--space-lg: 24px;
--space-xl: 40px;
}
}
/* Usage */
.section {
margin-bottom: var(--space-xl);
padding: var(--space-lg);
}
.section__title {
margin-bottom: var(--space-md);
}
.card-grid {
gap: var(--space-lg);
}
Component Encapsulation with Variables
Custom properties let you expose a clean API for your components. Rather than forcing consumers to override dozens of nested selectors, you provide a set of variables they can redefine. This pattern is common in design systems and component libraries.
/* Component author defines internal variables */
.alert {
--alert-bg: #fef2f2;
--alert-text: #991b1b;
--alert-border: #fecaca;
--alert-icon-color: var(--alert-text);
background-color: var(--alert-bg);
color: var(--alert-text);
border: 1px solid var(--alert-border);
}
.alert__icon {
color: var(--alert-icon-color);
}
/* Consumer can override the API */
.alert--success {
--alert-bg: #f0fdf4;
--alert-text: #166534;
--alert-border: #bbf7d0;
}
/* Even more targeted override */
.alert--warning {
--alert-bg: #fffbeb;
--alert-text: #854d0e;
--alert-border: #fde68a;
}
JavaScript-Driven Animations and Interactions
Because custom properties are accessible via JavaScript, you can bind them to dynamic events such as mouse position, scroll progress, or real-time data. This enables effects that would be cumbersome with inline styles or class toggling alone.
/* CSS sets up the foundation */
.gradient-card {
--mouse-x: 0.5;
--mouse-y: 0.5;
background: radial-gradient(
circle at calc(var(--mouse-x) * 100%) calc(var(--mouse-y) * 100%),
#3b82f6,
#1e40af
);
transition: --mouse-x 0.1s ease-out, --mouse-y 0.1s ease-out;
}
/* JavaScript updates the variables on mouse move */
document.querySelector('.gradient-card').addEventListener('mousemove', (e) => {
const rect = e.target.getBoundingClientRect();
const x = (e.clientX - rect.left) / rect.width;
const y = (e.clientY - rect.top) / rect.height;
e.target.style.setProperty('--mouse-x', x.toFixed(4));
e.target.style.setProperty('--mouse-y', y.toFixed(4));
});
Best Practices for CSS Custom Properties
1. Establish a Naming Convention Early
A clear naming convention prevents confusion and makes your codebase predictable. Common approaches include prefix-based systems (e.g., --color-, --space-, --font-) that group variables by category, or hierarchical naming that reflects the design token structure. Be consistent: if you start with --color-primary, don't later introduce --primaryColor.
/* Category-prefix convention — recommended */
:root {
/* Colors */
--color-primary: #3b82f6;
--color-primary-hover: #2563eb;
--color-surface: #ffffff;
--color-surface-alt: #f9fafb;
--color-text: #111827;
--color-text-muted: #6b7280;
/* Spacing */
--space-xs: 4px;
--space-sm: 8px;
--space-md: 16px;
--space-lg: 32px;
--space-xl: 64px;
/* Typography */
--font-size-sm: 0.875rem;
--font-size-base: 1rem;
--font-size-lg: 1.25rem;
--font-size-xl: 1.5rem;
/* Shadows */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
}
2. Prefer Global Variables on :root for Design Tokens
Place your foundational design tokens—colors, spacing, typography, breakpoints—on the :root pseudo-class. This makes them available everywhere and establishes a single source of truth. Component-specific variables should be scoped to the component's selector to avoid polluting the global namespace.
/* ✅ Good: Global tokens on :root */
:root {
--color-brand: #8b5cf6;
--space-gutter: 24px;
}
/* ✅ Good: Component-scoped variables */
.dropdown {
--dropdown-max-height: 300px;
--dropdown-animation-duration: 200ms;
}
/* ❌ Avoid: Component variables leaking globally */
:root {
--dropdown-max-height: 300px; /* Only relevant to dropdowns */
}
3. Use Fallbacks Defensively
Always consider providing fallback values when using var(), especially if the variable might not be defined in every context. This is particularly important for component libraries where consumers may not provide all expected variables.
/* With fallback — safe */
.button {
background-color: var(--button-bg, var(--color-primary, #007bff));
}
/* Without fallback — risky if --button-bg is undefined */
.button {
background-color: var(--button-bg);
}
4. Leverage calc() for Flexible Math
Custom properties shine when combined with calc(). You can derive values from base units, creating proportional relationships that remain consistent even when the base changes.
:root {
--base-spacing: 8px;
--base-font-size: 1rem;
}
.section {
/* Derive larger values from the base */
padding: calc(var(--base-spacing) * 4);
margin-bottom: calc(var(--base-spacing) * 6);
}
.heading {
/* Scale font sizes proportionally */
font-size: calc(var(--base-font-size) * 1.5);
line-height: calc(var(--base-font-size) * 2);
}
5. Document Your Variable API
When building components meant for reuse, document which custom properties the component exposes. This is the contract between your component and its consumers. Include both the variable names and their expected values in comments or a dedicated documentation file.
/*
* Button Component — Custom Property API
*
* --btn-bg Background color (default: var(--color-primary))
* --btn-text Text color (default: #ffffff)
* --btn-padding-x Horizontal padding (default: 16px)
* --btn-padding-y Vertical padding (default: 8px)
* --btn-radius Border radius (default: 6px)
* --btn-font-size Font size (default: var(--font-size-base))
*/
.btn {
background-color: var(--btn-bg, var(--color-primary));
color: var(--btn-text, #ffffff);
padding: var(--btn-padding-y, 8px) var(--btn-padding-x, 16px);
border-radius: var(--btn-radius, 6px);
font-size: var(--btn-font-size, var(--font-size-base));
}
6. Avoid Over-Nesting Variable References
While custom properties can reference other custom properties, deeply nested chains make debugging difficult. Keep the dependency graph shallow and predictable. If you find yourself chasing variables through multiple levels of indirection, consider flattening the structure.
/* ✅ Reasonable depth — one or two levels */
:root {
--color-primary: #3b82f6;
--button-bg: var(--color-primary);
--button-hover-bg: color-mix(in srgb, var(--button-bg) 80%, white);
}
/* ❌ Hard to debug — deeply nested chain */
:root {
--token-a: #111;
--token-b: var(--token-a);
--token-c: var(--token-b);
--token-d: var(--token-c);
--final-color: var(--token-d); /* What color is this actually? */
}
7. Use Variables for Transitions and Animations
Custom properties can be interpolated in CSS transitions and animations when you register them with @property (in supporting browsers). This allows you to animate properties that were previously not animatable, like gradient positions or clip-path values.
/* Register the property for animation */
@property --gradient-angle {
syntax: '';
initial-value: 0deg;
inherits: false;
}
.rotating-gradient {
--gradient-angle: 0deg;
background: linear-gradient(
var(--gradient-angle),
#3b82f6,
#8b5cf6,
#ec4899
);
transition: --gradient-angle 2s linear;
}
.rotating-gradient:hover {
--gradient-angle: 360deg;
}
8. Combine with Container Queries for Next-Level Adaptability
Custom properties pair beautifully with container queries. You can define variables at the container level and let child components adapt based on the container's size rather than the viewport, creating truly context-aware components.
.container {
container-type: inline-size;
}
@container (min-width: 500px) {
.container {
--card-layout: row;
--card-image-width: 200px;
--card-gap: 24px;
}
}
@container (max-width: 499px) {
.container {
--card-layout: column;
--card-image-width: 100%;
--card-gap: 16px;
}
}
.card {
display: flex;
flex-direction: var(--card-layout);
gap: var(--card-gap);
}
.card__image {
width: var(--card-image-width);
}
9. Test Fallback Chains in Multiple Browsers
Browser support for custom properties is excellent, but subtle differences exist in how fallbacks are resolved, especially when combined with calc() or complex shorthands. Always test your fallback logic across your target browsers, and consider using feature queries (@supports) for progressive enhancement scenarios.
/* Testing for custom property support */
@supports (color: var(--test, red)) {
.enhanced {
background: var(--gradient-start, #eee);
}
}
/* Fallback for older environments */
.card {
/* Older browsers get a static value */
background: #f5f5f5;
/* Modern browsers override with the variable */
background: var(--card-bg, #f5f5f5);
}
10. Keep Variables DRY but Not Overly Abstracted
The goal is to reduce repetition, not to create a labyrinth of indirection. Define a reasonable set of primitive tokens (colors, spacing units, font sizes) and then build semantic tokens on top of them. Semantic tokens map primitives to their purpose, like --color-text-heading referencing --color-neutral-900. This two-layer approach balances flexibility with clarity.
/* Layer 1: Primitives (raw values) */
:root {
--color-blue-500: #3b82f6;
--color-blue-600: #2563eb;
--color-neutral-50: #f9fafb;
--color-neutral-900: #111827;
--space-base: 8px;
}
/* Layer 2: Semantics (purpose-driven) */
:root {
--color-primary: var(--color-blue-500);
--color-primary-hover: var(--color-blue-600);
--color-surface: var(--color-neutral-50);
--color-text: var(--color-neutral-900);
--spacing-section: calc(var(--space-base) * 6);
}
/* Usage: always reference semantic tokens */
.heading {
color: var(--color-text);
margin-bottom: var(--spacing-section);
}
Debugging Custom Properties
When a custom property doesn't behave as expected, there are several tools at your disposal. Browser DevTools allow you to inspect computed values and see which rules are setting or overriding variables. The Computed panel in Chrome DevTools shows the final value of each custom property on the selected element. You can also use console.log(getComputedStyle(element).getPropertyValue('--my-var')) in JavaScript to inspect values at runtime. Remember that an empty fallback or a circular reference will cause the property to resolve to its initial value (or the fallback if provided).
/* Common pitfall: circular reference */
:root {
--size: var(--size); /* ❌ Circular — resolves to initial value */
}
/* Debugging in JavaScript */
const el = document.querySelector('.card');
const value = getComputedStyle(el).getPropertyValue('--card-bg');
console.log('--card-bg:', value || 'not set');
Conclusion
CSS custom properties have fundamentally transformed how we approach styling on the web. They bridge the gap between static stylesheets and dynamic, context-aware design systems. By mastering their syntax, understanding their behavior in the cascade, and following established best practices—consistent naming, defensive fallbacks, shallow dependency chains, and clear documentation—you can build maintainable, scalable, and adaptable stylesheets that respond gracefully to changing requirements. Whether you're theming an entire application, creating a reusable component library, or adding subtle interactivity with JavaScript, custom properties give you the tools to write CSS that feels both powerful and elegant. The key is to start small, build a solid token foundation, and let the cascade do the heavy lifting.