DEV Community

Weather Clock Dash
Weather Clock Dash

Posted on

CSS Custom Properties: Beyond Variables — Theming, Animation, and @property

CSS custom properties (CSS variables) are far more powerful than just storing colors. They cascade, inherit, can be updated with JavaScript, and enable dynamic theming without class swapping. Firefox has full support including @property for typed custom properties.

The Basics

:root {
  --color-primary: #3B82F6;
  --color-primary-dark: #2563EB;
  --spacing-sm: 8px;
  --spacing-md: 16px;
  --spacing-lg: 24px;
  --border-radius: 6px;
  --font-body: 'Inter', system-ui, sans-serif;
}

.button {
  background: var(--color-primary);
  padding: var(--spacing-sm) var(--spacing-md);
  border-radius: var(--border-radius);
  font-family: var(--font-body);
}

/* Fallback value (used if var is not defined) */
.button-alt {
  background: var(--color-secondary, #6B7280); /* fallback: gray */
}
Enter fullscreen mode Exit fullscreen mode

Cascading and Scoping

Custom properties cascade and can be overridden per scope:

/* Global default */
:root { --button-bg: #3B82F6; }

/* Override in dark mode */
@media (prefers-color-scheme: dark) {
  :root { --button-bg: #1D4ED8; }
}

/* Override for a specific component */
.card {
  --button-bg: #10B981; /* green buttons inside cards */
}

/* The button reads from wherever it is in the DOM */
.button {
  background: var(--button-bg);
}
Enter fullscreen mode Exit fullscreen mode

A .button inside .card gets green; outside, it gets blue. This is the cascade in action.

Theming with Custom Properties

The clean way to implement dark mode:

:root {
  --bg: #ffffff;
  --bg-secondary: #f9fafb;
  --text: #111827;
  --text-muted: #6B7280;
  --border: #E5E7EB;
  --card-shadow: 0 1px 3px rgba(0,0,0,0.1);
}

[data-theme="dark"] {
  --bg: #111827;
  --bg-secondary: #1F2937;
  --text: #F9FAFB;
  --text-muted: #9CA3AF;
  --border: #374151;
  --card-shadow: 0 1px 3px rgba(0,0,0,0.4);
}

/* All components use variables — zero style changes needed for dark mode */
body { background: var(--bg); color: var(--text); }
.card { background: var(--bg-secondary); border: 1px solid var(--border); }
Enter fullscreen mode Exit fullscreen mode
// Toggle theme
document.documentElement.dataset.theme = 'dark';
// Clear to reset to light
delete document.documentElement.dataset.theme;
Enter fullscreen mode Exit fullscreen mode

Updating Custom Properties with JavaScript

// Set a variable on the root
document.documentElement.style.setProperty('--spacing-unit', '4px');

// Set on a specific element (overrides for that element's subtree)
element.style.setProperty('--button-bg', '#EF4444');

// Read a variable's computed value
const value = getComputedStyle(document.documentElement)
  .getPropertyValue('--color-primary').trim();
Enter fullscreen mode Exit fullscreen mode

This enables dynamic theming without class manipulation:

// Color picker that updates CSS custom property
colorPicker.addEventListener('input', () => {
  document.documentElement.style.setProperty('--color-primary', colorPicker.value);
});
Enter fullscreen mode Exit fullscreen mode

Using Custom Properties in Calculations

:root {
  --grid-columns: 3;
  --grid-gap: 16px;
  --base-font-size: 16;
}

.grid {
  display: grid;
  grid-template-columns: repeat(var(--grid-columns), 1fr);
  gap: var(--grid-gap);
}

/* calc() with custom properties */
.sidebar {
  width: calc(100% / var(--grid-columns) - var(--grid-gap));
}

/* Convert unitless to rem */
.heading {
  font-size: calc(var(--base-font-size) * 1.5 * 1px); /* 24px */
}
Enter fullscreen mode Exit fullscreen mode

Animation with Custom Properties

Animating custom properties is powerful but has a catch: by default, CSS doesn't know the type of a custom property, so it can't interpolate between values.

Without @property (no animation):

:root { --progress: 0; }
/* Can't animate this directly — CSS doesn't know it's a number */
Enter fullscreen mode Exit fullscreen mode

With @property (animatable):

@property --progress {
  syntax: '<number>';
  inherits: false;
  initial-value: 0;
}

.progress-bar {
  --progress: 0;
  transform: scaleX(calc(var(--progress) / 100));
  transition: --progress 0.3s ease;
}

.progress-bar.loaded {
  --progress: 75;
}
Enter fullscreen mode Exit fullscreen mode

Now --progress smoothly animates from 0 to 75 because CSS knows it's a number.

@property Types

Firefox 128+ supports @property:

@property --hue {
  syntax: '<angle>';
  inherits: true;
  initial-value: 0deg;
}

@property --opacity {
  syntax: '<number>';
  inherits: false;
  initial-value: 1;
}

@property --color-primary {
  syntax: '<color>';
  inherits: true;
  initial-value: #3B82F6;
}

@property --size {
  syntax: '<length>';
  inherits: false;
  initial-value: 0px;
}
Enter fullscreen mode Exit fullscreen mode

Supported syntax types: <color>, <length>, <angle>, <number>, <integer>, <percentage>, <length-percentage>, <url>, <image>, <transform-list>, <custom-ident>, * (anything).

Component Customization API Pattern

Design systems use custom properties to expose a component's styling "API":

/* Button component — public API via custom properties */
.button {
  /* Defaults */
  --_btn-bg: var(--button-bg, #3B82F6);
  --_btn-text: var(--button-text, #ffffff);
  --_btn-radius: var(--button-radius, 6px);
  --_btn-padding: var(--button-padding, 8px 16px);

  background: var(--_btn-bg);
  color: var(--_btn-text);
  border-radius: var(--_btn-radius);
  padding: var(--_btn-padding);
}
Enter fullscreen mode Exit fullscreen mode

Users of the component can override via:

/* Override globally */
:root { --button-bg: #8B5CF6; } /* purple buttons */

/* Override locally */
.danger-zone { --button-bg: #EF4444; } /* red buttons in danger zone */
Enter fullscreen mode Exit fullscreen mode

No need to add variant classes — the component responds to its environment.

Debugging in Firefox DevTools

Firefox's Inspector shows all applied custom properties:

  1. Open Inspector → click an element
  2. In the Computed styles panel: search for -- to filter custom properties
  3. Properties show their resolved value
  4. Click the value to copy or edit it live

The Styles panel shows var() references with a small tooltip showing the resolved value when you hover.

Top comments (0)