EmpireUI
Get Pro
← Blog8 min read#css variables#@property#animation

Advanced CSS Custom Properties: @property, Animatable Tokens

Master @property, typed CSS tokens, and animatable custom properties to build transitions that vanilla CSS variables simply can't do.

Abstract colorful CSS gradient code on a dark monitor screen

Why Vanilla CSS Variables Hit a Wall

You've been using CSS custom properties since roughly 2017 — --color: #ff0066, done. They work great for theming. But try to animate one and you'll immediately hit the wall. The browser has no idea what --my-angle represents. Is it a number? A color? An angle? Without that context, transitions just snap at the end frame instead of interpolating.

That's the core problem @property solves. Landing in Chrome 85, Firefox 128 (behind a flag until 2024), and shipping without flags in all major browsers by 2025, the @property at-rule lets you register a typed custom property — one the browser actually understands well enough to tween between values. That changes everything for animation.

In practice, you'll notice the gap most when building gradient animations, hue rotations, or any transition that involves a value the browser normally treats as an opaque string. Swap in a registered property and the browser does the math correctly. No JavaScript, no workarounds.

Look, most UI component libraries paper over this with JS-driven animations. Empire UI takes a different approach — style tokens baked with @property registrations so you get CSS-native transitions on things like glassmorphism blur intensities and gradient stops. It's worth understanding the primitive before you reach for the abstraction.

The @property Syntax, Unpacked

The at-rule takes three required descriptors: syntax, inherits, and initial-value. Miss any one of them and the registration silently fails in most browsers.

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

The syntax descriptor accepts a subset of CSS value definition syntax. The most useful types you'll reach for day-to-day are <number>, <integer>, <length>, <percentage>, <angle>, <color>, <image>, and <transform-list>. You can also write '*' — that's the wildcard, it tells the browser to accept any value but won't enable interpolation. Worth noting: <color> is the one that unlocks the smoothest gradient tweens.

inherits: false means the property doesn't cascade down through the DOM tree. You almost always want this for animation tokens — otherwise a property you're tweening on a parent will bleed into children at unpredictable states. Set it to true only when you're using the property as a genuine theme token that descendants should consume.

One more thing — the initial-value has to be parseable by the syntax you declared. If you write syntax: '<angle>' and then initial-value: 0, the registration will fail because 0 without a unit isn't a valid angle (though 0deg is). Spend five minutes with the browser DevTools computed styles panel to confirm a registration landed correctly before you debug animations.

Animating Gradients Without JavaScript

Animating a CSS gradient used to mean swapping classes or driving values with requestAnimationFrame. Now you can do it entirely in CSS. The trick is registering the color stops or the hue values as typed properties, then transitioning them.

@property --stop-one {
  syntax: '<color>';
  inherits: false;
  initial-value: oklch(60% 0.3 260);
}

@property --stop-two {
  syntax: '<color>';
  inherits: false;
  initial-value: oklch(70% 0.3 320);
}

.animated-gradient {
  background: linear-gradient(135deg, var(--stop-one), var(--stop-two));
  transition: --stop-one 0.6s ease, --stop-two 0.6s ease;
}

.animated-gradient:hover {
  --stop-one: oklch(65% 0.35 180);
  --stop-two: oklch(75% 0.35 240);
}

Notice the use of oklch instead of hsl or hex. The oklch color space interpolates perceptually — you don't get that muddy grey-brown desaturated zone in the middle that rgb and hsl transitions produce. For UI work in 2026 you should default to oklch for any color that needs to animate. It's supported in all evergreen browsers.

This is exactly the pattern Empire UI uses under the hood for the gradient generator. You pick two stops, hit animate, and the live preview runs purely on CSS @property transitions — no canvas, no JS loop. You can inspect the generated output to see the registered properties directly.

Honestly, the first time you see a smooth gradient transition happening in 14 lines of CSS with zero JavaScript, you'll rethink how much animation work you've been offloading to libraries unnecessarily.

Typed Tokens for Design Systems

Here's where @property starts to feel genuinely system-scale. You can register your entire motion token layer as typed properties — easing curves as <custom-ident>, durations as <time>, spacing as <length> — and then animate individual component states by overriding those tokens at a lower scope.

/* Token registration — typically in :root or a dedicated tokens.css */
@property --duration-fast {
  syntax: '<time>';
  inherits: true;
  initial-value: 150ms;
}

@property --duration-moderate {
  syntax: '<time>';
  inherits: true;
  initial-value: 300ms;
}

@property --radius-card {
  syntax: '<length>';
  inherits: true;
  initial-value: 12px;
}

/* Component usage */
.card {
  border-radius: var(--radius-card);
  transition:
    border-radius var(--duration-moderate) ease,
    box-shadow var(--duration-fast) ease;
}

.card:hover {
  --radius-card: 20px;
}

The inherits: true here is intentional — you want --duration-fast to cascade so child elements can reference the parent's motion budget. Think of it like a font-size cascade but for motion.

Quick aside: TypeScript users can generate @property registrations automatically from a tokens JSON file. Run a small build script that maps { 'duration-fast': '150ms' } to the at-rule syntax and you'll keep your design system single-source-of-truth. The CSS Variables spec doesn't prescribe a tooling story here so the ecosystem has coalesced around this pattern organically.

One practical limit to know: you can't currently animate <transform-list> values in a composable way across multiple properties at once without careful specificity management. If you're stacking translate and rotate on the same element using separate @property tokens, test on Firefox — its interpolation order for <transform-list> differed from Chromium until mid-2025.

Keyframe Animations With Registered Properties

@property pairs with @keyframes just as naturally as it does with transition. This is how you get smooth infinite loops — spinning hue wheels, pulsing glows, breathing gradients — with zero layout thrashing.

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

@keyframes spin-hue {
  to { --hue: 360deg; }
}

.glow-ring {
  --hue: 0deg;
  box-shadow: 0 0 32px oklch(70% 0.35 var(--hue));
  animation: spin-hue 4s linear infinite;
}

Because --hue is typed as <angle>, the browser knows to interpolate through the value space correctly. You'll get a perfectly smooth 360-degree hue rotation without a single line of JavaScript. Compare that to the old approach — a requestAnimationFrame loop updating a CSS variable 60 times a second — and the elegance is hard to overstate.

This exact pattern is what drives the animated glow effects in several Empire UI cyberpunk components. The 32px spread radius and the oklch glow color are both registered tokens so you can override them per-component without touching the keyframe definition.

What about @media (prefers-reduced-motion)? Always wrap decorative infinite animations in a motion-preference guard. The @property registration itself is fine — it's just the animation declaration that should be conditional. Keep the static final state visible; hide only the motion.

JavaScript API: CSS.registerProperty

If you need to register properties programmatically — for instance inside a component that doesn't control a global stylesheet — there's CSS.registerProperty(). It mirrors the at-rule exactly, just as a JS call.

// Register once at the top of your module, or in a useEffect
if ('registerProperty' in CSS) {
  CSS.registerProperty({
    name: '--card-elevation',
    syntax: '<number>',
    inherits: false,
    initialValue: '0',
  });
}

// Then in your component's inline style or stylesheet
// .card { box-shadow: 0 calc(var(--card-elevation) * 4px) calc(var(--card-elevation) * 8px) rgba(0,0,0,0.1); }
// .card:hover { --card-elevation: 6; }

That if ('registerProperty' in CSS) guard matters. Older browser environments — some headless test runners, certain SSR contexts — will throw otherwise. It's also idempotent-safe in Chromium but throws a DOMException if you call it twice with the same property name in Firefox, so wrap repeat registrations in a try/catch.

That said, prefer the at-rule version whenever you control the stylesheet. At-rule declarations are parsed earlier in the pipeline and don't depend on script execution order. JavaScript registration is for dynamic cases — user-generated themes, runtime component composition, polyfill shims.

If you're building a design tool or a live preview UI similar to Empire UI's box shadow generator, the JS API is actually the right call: you can register new properties on-the-fly as users define animation parameters, then clean them up when they switch modes.

Browser Support, Gotchas, and What's Next

As of 2026, @property is green across Chrome, Edge, Firefox, and Safari. The caveats are in the details: Firefox added full support in version 128 and doesn't support all syntax descriptors equally — <transform-list> animation is still inconsistent. Safari has supported @property since version 16.4 but had a bug with <color> initial values using currentColor that persisted until 17.2.

Progressive enhancement is straightforward. Unregistered custom properties still work as plain string tokens — they just won't animate. So your fallback is always graceful: components render correctly, they just transition without interpolation on unsupported browsers. Write your base styles assuming no @property, then layer the typed registration on top.

On the horizon: the CSS Values Level 5 spec is adding @property support for <ratio>, which will let you animate aspect ratios. There's also ongoing work on inherits: selector(...) — a way to scope inheritance to specific subtree relationships rather than the whole cascade. Both are at working-draft stage as of mid-2026.

One area the community is exploring is using @property to build animatable color scheme switching — registering --foreground and --background as <color> tokens and transitioning the dark/light swap rather than flipping it instantaneously. It works beautifully with the aurora style tokens in Empire UI where the background gradient shifts between modes.

FAQ

Can I animate CSS custom properties without @property?

Not natively — without a type registration the browser treats the value as a string and just swaps it at the end of the transition. You'd need JavaScript to poll and update the value each frame, which works but defeats the point of CSS transitions.

Does @property work in Next.js and Tailwind projects?

Yes. Drop the @property declarations into your globals.css before any layer imports. Tailwind doesn't interfere with at-rules. The only gotcha is that PostCSS plugins that aggressively purge or transform CSS sometimes strip unknown at-rules — check your PostCSS config if registrations mysteriously disappear.

What's the difference between @property and CSS.registerProperty()?

They're functionally identical — same descriptors, same browser behavior. Use the at-rule in stylesheets (parsed earlier, no JS dependency) and the JS API when you need to register properties dynamically at runtime, like in a component that generates its own animation tokens.

Why use oklch instead of hsl for animated color properties?

The hsl color space produces visually uneven lightness as you rotate hue — you get brighter and duller zones. oklch has perceptually uniform lightness so the transition looks consistent all the way around. Browser support for oklch in @property initial values landed in Chrome 111 and Safari 16.4.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

CSS Custom Properties: Dynamic Theming and Animation TricksFigma Variables to CSS Custom Properties: The Workflow That WorksCSS Custom Properties as a Design System: The Right ArchitectureMotion Design Tokens: Systematising Easing, Duration and Delay