EdgeCases Logo
Feb 2026
CSS
Expert
7 min read

CSS @property Animation: The Compositor Can't Help You

Typed custom properties enable smooth interpolation but force main-thread animation—here's what to watch for

css
@property
animation
custom-properties
houdini
performance
edge-case

Register a custom property with @property, animate it smoothly, and watch your performance profile turn red. CSS custom properties don't animate on the compositor—every frame triggers style recalculation and repaint. Here's what the tutorials don't tell you about @property animations.

The Promise: Typed Custom Properties

/* Unregistered: discrete animation (jumps at 50%) */
.box {
  --angle: 0deg;
  rotate: var(--angle);
  transition: --angle 1s;
}

.box:hover {
  --angle: 360deg; /* Jumps from 0 to 360 halfway through */
}

/* Registered: smooth interpolation */
@property --angle {
  syntax: '<angle>';
  initial-value: 0deg;
  inherits: false;
}

.box {
  --angle: 0deg;
  rotate: var(--angle);
  transition: --angle 1s;
}

.box:hover {
  --angle: 360deg; /* Smoothly interpolates! */
}

By declaring the type (<angle>), the browser knows how to interpolate values. This enables smooth transitions for colors, lengths, percentages, and more—previously impossible with untyped custom properties.

The Reality: Main Thread Only

/* This animates smoothly but NOT on the compositor */
@property --hue {
  syntax: '<number>';
  initial-value: 0;
  inherits: false;
}

@keyframes cycle-hue {
  to { --hue: 360; }
}

.rainbow {
  animation: cycle-hue 5s linear infinite;
  background: hsl(var(--hue) 70% 50%);
}

Open DevTools Performance panel. You'll see continuous style recalculations and repaints every frame. Compare to a native rotate animation—that runs on the compositor with zero main thread work.

Why Compositor Can't Handle It

The compositor runs in a separate thread and only understands a fixed set of properties: transform, opacity, and a few others. Custom properties require var() substitution, which happens during style computation—strictly a main thread operation.

/* When --angle changes: */
// 1. Invalidate styles (--angle changed)
// 2. Recompute styles (substitute var(--angle))
// 3. Recalculate layout (if affected)
// 4. Repaint

/* vs native rotate animation: */
// Compositor thread handles it entirely
// Main thread stays idle

The browser can't pre-compute all possible var() substitutions on the compositor. The dependency graph is too complex—a custom property might affect any number of declarations.

Registration Gotchas

/* Gotcha 1: Can't re-register */
@property --color {
  syntax: '<color>';
  initial-value: red;
  inherits: false;
}

@property --color {
  syntax: '<color>';
  initial-value: blue; /* Ignored! First registration wins */
  inherits: false;
}

/* Gotcha 2: Validation at computed time, not parse time */
.box {
  --color: not-a-color; /* No parse error! */
  /* Falls back to initial-value at compute time */
  background: var(--color); /* red, not not-a-color */
}

/* Gotcha 3: Invalid after valid doesn't cascade */
.box {
  --color: blue;
  --color: not-a-color; /* Invalid, but doesn't fall back to blue! */
  background: var(--color); /* initial-value (red), NOT blue */
}

Unlike standard CSS where invalid values fall back to the previous valid declaration, registered custom properties fall back to their initial-value. This breaks the normal cascade expectation.

Inheritance Surprises

@property --spacing {
  syntax: '<length>';
  initial-value: 0px;
  inherits: false; /* Crucial */
}

.parent {
  --spacing: 20px;
}

.child {
  /* --spacing is 0px, not 20px! */
  /* inherits: false means each element starts fresh */
  padding: var(--spacing);
}

Most tutorials show inherits: false without explaining the consequence. Set it to true if you want normal CSS inheritance behavior.

Performance-Safe Patterns

/* ❌ Avoid: animating @property for visual effects */
@property --progress {
  syntax: '<percentage>';
  initial-value: 0%;
  inherits: false;
}

@keyframes load {
  to { --progress: 100%; }
}

.loader {
  animation: load 2s;
  width: var(--progress); /* Main thread every frame */
}

/* ✅ Better: use native properties */
@keyframes load {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}

.loader {
  animation: load 2s;
  transform-origin: left;
}

Reserve @property animations for cases where you need the typed interpolation (color gradients, complex calculations) and can't achieve the effect with compositor-friendly properties.

Valid Use Cases

/* Gradient color transitions (impossible otherwise) */
@property --gradient-stop {
  syntax: '<color>';
  initial-value: #ff0000;
  inherits: false;
}

.card {
  --gradient-stop: #ff0000;
  background: linear-gradient(to right, #000, var(--gradient-stop));
  transition: --gradient-stop 0.3s;
}

.card:hover {
  --gradient-stop: #00ff00; /* Smooth color transition in gradient */
}

/* Calculations that need interpolation */
@property --radius {
  syntax: '<length>';
  initial-value: 0px;
  inherits: false;
}

.morphing-shape {
  --radius: 0px;
  border-radius: calc(var(--radius) + 10px);
  transition: --radius 0.5s;
}

These use cases accept the main-thread cost because there's no alternative—you can't animate gradient colors or complex calc() expressions any other way.

Browser Support Status

@property is supported in Chrome 85+, Edge 85+, Safari 16.4+, and Firefox 128+. The performance limitation exists in all implementations—it's a fundamental constraint, not a browser bug.

Key Takeaways

  • Custom property animations run on the main thread, not the compositor
  • Every frame triggers style recalc and repaint—profile your animations
  • Registration is one-time: first @property declaration wins
  • Invalid values fall back to initial-value, not the previous valid value
  • inherits: false breaks normal CSS inheritance—use true when needed
  • Use @property for typed interpolation only when compositor properties can't work

Advertisement

Related Insights

Explore related edge cases and patterns

TypeScript
Expert
TypeScript Performance: Recursive Types & Build Times
6 min
SEO
Deep
Canonical URLs: The Duplicate Content Paradox
7 min
SEO
Expert
Internal Linking Architecture: PageRank Distribution in Component-Based Apps
7 min
Performance
Deep
The Web Animation Performance Tier List
6 min

Advertisement