⚡IntermediateCSS Variables

CSS Custom Properties Advanced

Build theming systems, component tokens, and JavaScript-driven animations using cascading custom properties, fallbacks, and @property.

Overview

CSS custom properties (variables) go far beyond basic `--color: blue`. Their real power comes from understanding the cascade, scope, and JavaScript interoperability. **Key advanced patterns:** 1. **Cascading scope** a `:root` variable is overridden when you redeclare it on a child element 2. **Fallbacks** `var(--color, #6366f1)` uses the fallback if the variable is undefined 3. **Component-level theming** override tokens inside a component without changing global styles 4. **JavaScript interop** read with `getComputedStyle`, write with `el.style.setProperty` 5. **`@property`** register a typed custom property that can be animated The cascade is what makes component theming possible: a variable defined closer to the element takes precedence over one defined further away (just like regular CSS specificity for normal properties).

Key Points

  • •var(--color, fallback) uses the fallback if --color is not defined or is invalid
  • •Custom properties inherit by default a child gets the parent's variable value
  • •Redefine a variable on a more specific selector to "scope" it to that component
  • •JavaScript reads: getComputedStyle(el).getPropertyValue("--my-var")
  • •JavaScript writes: el.style.setProperty("--my-var", "value")
  • •@property allows defining a type, syntax, and initial value for a custom property
  • •@property makes custom properties animatable (without it, animation snaps instantly)

Examples

Component-level theme overrides via cascade

:root {
  --card-bg: white;
  --card-border: #e5e7eb;
  --btn-color: #6366f1;
}

.card {
  background: var(--card-bg);
  border: 1px solid var(--card-border);
}

/* Dark variant override tokens locally */
.card-dark {
  --card-bg: #0f172a;
  --card-border: #1e293b;
  --btn-color: #818cf8;
  color: white;
}

.card-dark .btn {
  background: var(--btn-color);
  /* automatically uses the dark-variant value */
}

No selector specificity tricks needed just override the variable in the right scope.

Animatable property with @property

@property --gradient-angle {
  syntax: '<angle>';         /* typed as an angle */
  initial-value: 0deg;
  inherits: false;
}

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

.animated-card {
  background: conic-gradient(
    from var(--gradient-angle),
    #6366f1, #8b5cf6, #ec4899, #6366f1
  );
  animation: rotateGradient 4s linear infinite;
}

Without @property, animating custom properties in keyframes snaps between values (no interpolation). @property adds type information that enables smooth animation.

JavaScript-driven interactive styles

/* CSS receives the variable */
.interactive {
  background: hsl(var(--hue, 240) 60% 60%);
  transition: background 0.2s;
}

/* JavaScript writes the variable */
// document.addEventListener('mousemove', (e) => {
//   const hue = Math.round(e.clientX / window.innerWidth * 360);
//   document.documentElement.style.setProperty('--hue', hue);
// });

This pattern powers cursor-following gradients, scroll-based animations, and real-time theme editors.

Common Mistakes

  • Using var(--undefined-var) without a fallback the declaration becomes invalid, not just empty
  • Expecting @property to animate in browsers that do not support it use @supports to check
  • Overusing global :root variables for everything some tokens belong in component scope for better encapsulation

Note

Use a naming convention like --color-brand, --color-text-muted, --space-md, --radius-sm for your token names. This self-documents your design system and makes it clear what each variable controls.

Exercises

Write CSS to style the HTML scaffold. The preview updates live — when it looks right, mark as done.

Loading editor…

Completed: 0 / 5 exercises

Knowledge Check

Question 1 of 4

What happens when you redefine a CSS custom property on a child element?