Custom Properties

Difficulty: Advanced

CSS Custom Properties (also called CSS Variables) are entities defined by authors that contain specific values to be reused throughout a document. Defined using the -- prefix (e.g., --primary-color: #3b82f6) and accessed using the var() function, they bring the power of variables to CSS without needing a preprocessor like Sass or Less. Unlike preprocessor variables that are compiled away at build time, custom properties are live in the browser - they cascade, inherit, can be scoped, and can be changed dynamically with JavaScript.

Custom properties are typically defined on the :root pseudo-class (which represents the <html> element) to make them globally available. However, they follow the same cascade and inheritance rules as any CSS property. A custom property defined on a specific element or class overrides the global value within that scope. This scoping behavior is what makes custom properties incredibly powerful for component-based architectures - a card component can define its own --card-padding that only affects cards, without polluting the global namespace.

The var() function retrieves the value of a custom property. It accepts an optional second argument as a fallback value: var(--color, #333) uses #333 if --color is not defined. Fallback values can themselves contain var() references, enabling chains like var(--theme-color, var(--default-color, blue)). If a custom property is set to an invalid value for the property it is used in, the property receives its inherited value (not the fallback) - this is called the "invalid at computed-value time" behavior.

Dynamic theming is the most powerful application of custom properties. By defining a theme as a set of custom properties on :root, you can switch themes by changing just the variable values - either through a class toggle on the <html> element or via JavaScript's element.style.setProperty(). Dark mode becomes trivial: define light theme variables on :root and dark theme variables on :root.dark or [data-theme="dark"], then toggle the class or attribute. Every element referencing those variables updates automatically.

Custom properties can store any valid CSS value: colors, lengths, numbers, strings, and even entire property values like a complete box-shadow or gradient. They can be used in calc() expressions (calc(var(--spacing) * 2)), combined with other values (border: var(--border-width) solid var(--border-color)), and even animated in modern browsers that support @property for registered custom properties. The @property at-rule lets you declare the type, inheritance, and initial value of a custom property, enabling smooth transitions between values.

Code examples

Defining and Using Custom Properties

/* Define global custom properties on :root */
:root {
  --color-primary: #3b82f6;
  --color-secondary: #8b5cf6;
  --color-text: #1f2937;
  --color-bg: #ffffff;
  --spacing-sm: 8px;
  --spacing-md: 16px;
  --spacing-lg: 32px;
  --radius: 8px;
  --shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  --font-body: 'Inter', system-ui, sans-serif;
}

/* Using custom properties */
.card {
  background: var(--color-bg);
  color: var(--color-text);
  padding: var(--spacing-lg);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  font-family: var(--font-body);
}

.button {
  background: var(--color-primary);
  color: white;
  padding: var(--spacing-sm) var(--spacing-md);
  border-radius: var(--radius);
  border: none;
}

/* Fallback values */
.element {
  color: var(--accent-color, #e11d48);
  font-size: var(--custom-size, 16px);
  margin: var(--custom-margin, var(--spacing-md));
}

Custom properties defined on :root are globally available. The var() function retrieves values with optional fallbacks. Fallbacks can be nested var() calls. Once defined, changing a single variable value updates every element that references it.

Scoping Custom Properties

/* Global defaults */
:root {
  --button-bg: #3b82f6;
  --button-text: #ffffff;
  --button-padding: 10px 20px;
  --button-radius: 6px;
}

/* Base button uses global variables */
.button {
  background: var(--button-bg);
  color: var(--button-text);
  padding: var(--button-padding);
  border-radius: var(--button-radius);
  border: none;
  cursor: pointer;
}

/* Variant overrides via scoped custom properties */
.button-danger {
  --button-bg: #ef4444;
  --button-text: #ffffff;
}

.button-outline {
  --button-bg: transparent;
  --button-text: #3b82f6;
  border: 2px solid var(--button-text);
}

.button-large {
  --button-padding: 14px 32px;
  --button-radius: 10px;
  font-size: 18px;
}

/* Component-scoped variables */
.card {
  --card-padding: 24px;
  --card-bg: white;
  padding: var(--card-padding);
  background: var(--card-bg);
}

.card.compact {
  --card-padding: 12px;
}

Custom properties cascade and inherit like regular CSS. Defining a variable on a specific class overrides it within that element's scope. This enables component variants without duplicating property declarations - just change the variable values.

Dark Mode with Custom Properties

/* Light theme (default) */
:root {
  --bg-primary: #ffffff;
  --bg-secondary: #f3f4f6;
  --text-primary: #111827;
  --text-secondary: #6b7280;
  --border-color: #e5e7eb;
  --shadow-color: rgba(0, 0, 0, 0.1);
  --accent: #3b82f6;
}

/* Dark theme */
:root[data-theme="dark"] {
  --bg-primary: #111827;
  --bg-secondary: #1f2937;
  --text-primary: #f9fafb;
  --text-secondary: #9ca3af;
  --border-color: #374151;
  --shadow-color: rgba(0, 0, 0, 0.4);
  --accent: #60a5fa;
}

/* Auto dark mode based on system preference */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg-primary: #111827;
    --bg-secondary: #1f2937;
    --text-primary: #f9fafb;
    --text-secondary: #9ca3af;
    --border-color: #374151;
    --shadow-color: rgba(0, 0, 0, 0.4);
    --accent: #60a5fa;
  }
}

/* Components reference variables - no duplication needed */
body {
  background: var(--bg-primary);
  color: var(--text-primary);
}

.card {
  background: var(--bg-secondary);
  border: 1px solid var(--border-color);
  box-shadow: 0 2px 8px var(--shadow-color);
}

/* Toggle theme with JavaScript */
<script>
function toggleTheme() {
  const current = document.documentElement.getAttribute('data-theme');
  const next = current === 'dark' ? 'light' : 'dark';
  document.documentElement.setAttribute('data-theme', next);
  localStorage.setItem('theme', next);
}
</script>

Define your entire color palette as custom properties. Dark mode just redefines those variables - all components automatically adapt without any additional CSS. The data-theme attribute approach allows manual toggle that overrides system preference.

Dynamic Properties with JavaScript and @property

/* Change custom properties with JavaScript */
<script>
const root = document.documentElement;

// Set a custom property
root.style.setProperty('--accent', '#e11d48');

// Read a custom property
const accent = getComputedStyle(root).getPropertyValue('--accent');
console.log(accent); // #e11d48

// Remove a custom property (revert to inherited/fallback)
root.style.removeProperty('--accent');

// Dynamic spacing based on input
function updateSpacing(value) {
  root.style.setProperty('--spacing-unit', value + 'px');
}
</script>

/* @property: Register typed custom properties (enables animation) */
@property --gradient-angle {
  syntax: '<angle>';
  initial-value: 0deg;
  inherits: false;
}

.gradient-box {
  --gradient-angle: 0deg;
  background: conic-gradient(
    from var(--gradient-angle),
    #3b82f6,
    #8b5cf6,
    #ec4899,
    #3b82f6
  );
  transition: --gradient-angle 0.5s ease;
}

.gradient-box:hover {
  --gradient-angle: 180deg;
}

/* Animating custom properties with @keyframes */
@keyframes rotateGradient {
  to { --gradient-angle: 360deg; }
}

.animated-gradient {
  animation: rotateGradient 3s linear infinite;
}

JavaScript can read, set, and remove custom properties in real time. The @property at-rule registers a custom property with a specific type (angle, color, length, etc.), which enables the browser to interpolate and animate between values - something not possible with unregistered custom properties.

Key points

Concepts covered

var(), :root, Fallback Values, Scope, Dynamic Theming