Intermediate11 min read

CSS var() Function and Custom Properties Deep Dive

11 min read
351 words
17 sections16 code blocks

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.

CSS
: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.

CSS
.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.

CSS
.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.

CSS
.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.

CSS
.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.

CSS
: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.

CSS
.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.

CSS
: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.

CSS
: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.

CSS
: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.

CSS
: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%
  );
}
JavaScript
// 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.

CSS
.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

CSS
@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

CSS
: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.

CSS
: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.