DEV Community

Weather Clock Dash
Weather Clock Dash

Posted on

CSS Custom Properties Deep Dive: Inheritance, @property, JS Integration, Dark Mode, and Design Systems

CSS Custom Properties (CSS Variables) have fundamentally changed how we write CSS. Combined with Firefox's DevTools, they enable dynamic theming, design system management, and live visual debugging that wasn't possible before. This guide covers the technical depth: inheritance, fallbacks, JS interaction, and debugging in Firefox DevTools.

What Makes CSS Variables Different

CSS variables are:

  1. Inherited by default — a variable set on :root is available anywhere in the document
  2. Dynamic — changing them via JavaScript updates all dependent values immediately
  3. Cascadeable — they follow the same cascade rules as other CSS properties

Unlike preprocessor variables (Sass $color, Less @color), CSS custom properties:

  • Exist at runtime, not just at compile time
  • Can be updated via JavaScript
  • Work with the browser's native cascade and inheritance
  • Don't require a build step

Basic Syntax

/* Define on :root for global scope */
:root {
  --color-primary: #0066cc;
  --spacing-base: 1rem;
  --font-size-large: 1.5rem;
}

/* Use with var() */
.button {
  background-color: var(--color-primary);
  padding: var(--spacing-base);
}
Enter fullscreen mode Exit fullscreen mode

Fallback Values

var() accepts a fallback as the second argument:

.element {
  /* Uses --custom-color if defined, falls back to red */
  color: var(--custom-color, red);

  /* Fallback can itself use var() */
  font-size: var(--font-size-override, var(--font-size-base, 1rem));
}
Enter fullscreen mode Exit fullscreen mode

The fallback is only used if the variable is not defined — not if it's invalid. An invalid variable value causes the property to use its inherited value, not the fallback.

Scoped Variables

Variables can be scoped to specific elements:

:root {
  --button-color: blue;
}

/* Override for elements inside .dark-section */
.dark-section {
  --button-color: lightblue;
}

button {
  background: var(--button-color); /* blue normally, lightblue inside .dark-section */
}
Enter fullscreen mode Exit fullscreen mode

This is how CSS-variable-based theming works: redefine variables on the root for theme switching.

Dark/Light Mode with CSS Variables

:root {
  --bg: #ffffff;
  --text: #000000;
  --accent: #0066cc;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #1a1a1a;
    --text: #f0f0f0;
    --accent: #4da3ff;
  }
}

body {
  background: var(--bg);
  color: var(--text);
}
a {
  color: var(--accent);
}
Enter fullscreen mode Exit fullscreen mode

All elements using these variables automatically update when the system dark mode toggles.

JavaScript Interaction

// Read a CSS variable's value
const style = getComputedStyle(document.documentElement);
const primary = style.getPropertyValue('--color-primary').trim();
// Returns '#0066cc' (with possible leading spaces)

// Set a CSS variable via JavaScript
document.documentElement.style.setProperty('--color-primary', '#ff0000');
// All elements using --color-primary update immediately

// Remove a CSS variable (reverts to :root definition)
document.documentElement.style.removeProperty('--color-primary');

// Set on a specific element
const element = document.getElementById('hero');
element.style.setProperty('--highlight', 'yellow');
Enter fullscreen mode Exit fullscreen mode

This pattern enables JavaScript-driven themes without updating class names.

Computed vs. Inherited Values

// getPropertyValue returns the defined value at the current scope
const rootStyle = getComputedStyle(document.documentElement);
rootStyle.getPropertyValue('--spacing') // returns '1rem' (raw string)

// But the computed value of a property using the variable:
const button = document.querySelector('.button');
getComputedStyle(button).getPropertyValue('padding')
// returns actual computed padding, e.g., '16px' (not 'var(--spacing)')
Enter fullscreen mode Exit fullscreen mode

Animation with CSS Variables

CSS variables can be animated, but with a caveat — you need @property to declare the type:

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

@keyframes rotate-gradient {
  to {
    --gradient-angle: 360deg;
  }
}

.animated-gradient {
  background: conic-gradient(from var(--gradient-angle), red, blue, red);
  animation: rotate-gradient 3s linear infinite;
}
Enter fullscreen mode Exit fullscreen mode

Without @property, the browser treats variables as strings and can't interpolate them for smooth animation.

@property is supported in Firefox 128+ (experimental in earlier versions).

Firefox DevTools: Editing CSS Variables Live

In Firefox's Inspector → Rules panel:

  • Variables defined with -- prefix appear in the rules list
  • Click the value to edit inline — all elements using that variable update in real time
  • The color swatch appears next to color variable values — click for a color picker

Computed panel: variables don't appear in the Computed panel (they're not computed properties), but properties using them show their resolved values.

Console debugging:

// In Firefox DevTools console
getComputedStyle(document.documentElement).getPropertyValue('--color-primary')
Enter fullscreen mode Exit fullscreen mode

CSS Variables and Design Systems

Large codebases often use CSS variables as a design token layer:

/* Primitive tokens */
:root {
  --blue-400: #60a5fa;
  --blue-600: #2563eb;
  --gray-100: #f3f4f6;
  --gray-900: #111827;
}

/* Semantic tokens (context-meaningful names) */
:root {
  --color-action: var(--blue-600);
  --color-action-light: var(--blue-400);
  --color-background: var(--gray-100);
  --color-text: var(--gray-900);
}

/* Components use semantic tokens */
.button-primary {
  background: var(--color-action);
  color: var(--color-background);
}
Enter fullscreen mode Exit fullscreen mode

Components never reference primitive tokens directly — only semantic ones. Changing the theme means redefining semantic tokens, not touching component CSS.

Performance Considerations

CSS variables have minimal performance overhead in modern browsers. They're computed at paint time — changing a variable triggers a repaint of affected elements, not a full layout recalculation (unless the variable affects layout properties like width or margin).

Avoid changing many variables simultaneously via JavaScript in a tight loop. Batch changes using requestAnimationFrame:

requestAnimationFrame(() => {
  document.documentElement.style.setProperty('--header-height', newHeight + 'px');
  document.documentElement.style.setProperty('--content-top', (newHeight + 16) + 'px');
});
Enter fullscreen mode Exit fullscreen mode

Top comments (0)