CSS var() Function and Custom Properties Deep Dive
The var() function is the key to unlocking the power of CSS Custom Properties (also known as CSS Variables). Unlike preprocessor variables that are compiled away, CSS custom properties are live values that exist at runtime, enabling dynamic styling, theming, and component-based architectures.
Defining Custom Properties
Custom properties are defined using the -- prefix and are typically placed on the :root selector for global scope, though they can be defined on any element.
:root {
--primary-color: #3b82f6;
--secondary-color: #10b981;
--font-size-base: 1rem;
--spacing-unit: 8px;
--border-radius: 4px;
--transition-speed: 200ms;
}Using var() to Access Custom Properties
The var() function retrieves the value of a custom property. If the property doesn't exist or is invalid, you can provide a fallback value.
.button {
background-color: var(--primary-color);
padding: var(--spacing-unit);
border-radius: var(--border-radius);
transition: background-color var(--transition-speed);
}Fallback Values
The var() function accepts an optional second argument: a fallback value used when the custom property is undefined or invalid.
.element {
/* If --accent-color is not defined, use #ff6b6b */
color: var(--accent-color, #ff6b6b);
/* Fallback can be another variable */
background: var(--bg-color, var(--primary-color, blue));
/* Complex fallback */
font-size: var(--custom-size, 1rem);
}Multiple Fallback Levels
You can chain var() functions for multiple fallback levels.
.text {
color: var(--text-color, var(--body-color, var(--default-color, black)));
}Important Fallback Gotcha
The fallback value is everything after the first comma, which means commas in the fallback are included.
.element {
/* The fallback is "Helvetica, Arial, sans-serif" */
font-family: var(--font-stack, Helvetica, Arial, sans-serif);
/* The fallback is "1px solid red" */
border: var(--border, 1px solid red);
}Scope and Inheritance
Custom properties follow the CSS cascade and are inherited by default. This enables powerful scoping patterns.
:root {
--spacing: 1rem;
}
.compact {
--spacing: 0.5rem;
}
.spacious {
--spacing: 2rem;
}
/* All children of .compact will use 0.5rem spacing */
.card {
padding: var(--spacing);
margin-bottom: var(--spacing);
}Component-Scoped Variables
Define variables at the component level for encapsulated, reusable styles.
.card {
--card-padding: 1.5rem;
--card-bg: white;
--card-border: 1px solid #e5e7eb;
--card-radius: 8px;
--card-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: var(--card-padding);
background: var(--card-bg);
border: var(--card-border);
border-radius: var(--card-radius);
box-shadow: var(--card-shadow);
}
/* Variations override component variables */
.card--dark {
--card-bg: #1f2937;
--card-border: 1px solid #374151;
}
.card--elevated {
--card-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}Dynamic Theming
One of the most powerful applications of custom properties is creating theme systems that can be switched dynamically.
:root {
/* Light theme (default) */
--color-bg: #ffffff;
--color-text: #1f2937;
--color-text-muted: #6b7280;
--color-primary: #3b82f6;
--color-border: #e5e7eb;
}
[data-theme="dark"] {
--color-bg: #111827;
--color-text: #f9fafb;
--color-text-muted: #9ca3af;
--color-primary: #60a5fa;
--color-border: #374151;
}
/* Components use variables */
body {
background-color: var(--color-bg);
color: var(--color-text);
}
a {
color: var(--color-primary);
}
.border {
border-color: var(--color-border);
}System Preference Detection
Combine with prefers-color-scheme for automatic theme detection.
:root {
--color-bg: #ffffff;
--color-text: #1f2937;
}
@media (prefers-color-scheme: dark) {
:root {
--color-bg: #111827;
--color-text: #f9fafb;
}
}
/* Override with explicit user preference */
[data-theme="light"] {
--color-bg: #ffffff;
--color-text: #1f2937;
}
[data-theme="dark"] {
--color-bg: #111827;
--color-text: #f9fafb;
}Using var() with calc()
Custom properties work seamlessly with calc() for dynamic calculations.
:root {
--base-size: 16px;
--scale-ratio: 1.25;
--spacing-unit: 8px;
}
h1 {
font-size: calc(var(--base-size) * var(--scale-ratio) * var(--scale-ratio) * var(--scale-ratio));
}
h2 {
font-size: calc(var(--base-size) * var(--scale-ratio) * var(--scale-ratio));
}
.section {
padding: calc(var(--spacing-unit) * 4);
margin-bottom: calc(var(--spacing-unit) * 6);
}JavaScript Integration
Custom properties can be read and modified with JavaScript, enabling truly dynamic styling.
:root {
--mouse-x: 50%;
--mouse-y: 50%;
--user-color: #3b82f6;
}
.spotlight {
background: radial-gradient(
circle at var(--mouse-x) var(--mouse-y),
rgba(255, 255, 255, 0.2),
transparent 50%
);
}// Reading a custom property
const styles = getComputedStyle(document.documentElement);
const primaryColor = styles.getPropertyValue('--primary-color');
// Setting a custom property
document.documentElement.style.setProperty('--primary-color', '#ff0000');
// Dynamic mouse tracking
document.addEventListener('mousemove', (e) => {
const x = (e.clientX / window.innerWidth) * 100;
const y = (e.clientY / window.innerHeight) * 100;
document.documentElement.style.setProperty('--mouse-x', x + '%');
document.documentElement.style.setProperty('--mouse-y', y + '%');
});Advanced Patterns
Conditional Styling with Space Toggle
A clever hack using custom properties for conditional styling.
.element {
--is-active: ; /* Empty by default */
background: var(--is-active, transparent);
}
.element.active {
--is-active: initial; /* Activates the property */
background: var(--is-active, blue); /* Now uses blue */
}Animation with Custom Properties
@keyframes pulse {
0%, 100% {
--scale: 1;
}
50% {
--scale: 1.1;
}
}
.pulsing {
animation: pulse 2s infinite;
transform: scale(var(--scale, 1));
}
/* Note: Custom properties aren't directly animatable,
but @property can register them as animatable */
@property --scale {
syntax: '<number>';
initial-value: 1;
inherits: false;
}Design Token System
:root {
/* Primitive tokens */
--blue-50: #eff6ff;
--blue-500: #3b82f6;
--blue-900: #1e3a8a;
/* Semantic tokens */
--color-primary: var(--blue-500);
--color-primary-light: var(--blue-50);
--color-primary-dark: var(--blue-900);
/* Component tokens */
--button-bg: var(--color-primary);
--button-hover-bg: var(--color-primary-dark);
}Browser Support and Invalid Values
When a custom property contains an invalid value for a property, the browser uses the initial value for that property rather than the fallback.
:root {
--invalid-color: 45deg; /* Not a valid color */
}
.element {
/* Results in 'transparent' (initial value), not red */
background-color: var(--invalid-color, red);
}To handle this, ensure your variables contain appropriate values or use @supports.
Summary
The var() function transforms CSS into a more dynamic, maintainable language. Combined with custom properties, it enables powerful theming systems, component-based architectures, and seamless JavaScript integration. Understanding scope, inheritance, and fallback behavior is essential for building robust, scalable CSS systems.