
@property: Animating CSS Custom Properties
CSS custom properties (variables) are not animatable by default. The browser treats them as opaque strings — it has no idea whether --my-value holds a color, a length, or a number, so it can’t interpolate between two values. You get a hard snap instead of a smooth transition.
@property (part of CSS Houdini’s Properties and Values API) fixes this by registering a custom property with an explicit type, syntax, and initial value. Once the browser knows the type, it can interpolate.
@property --hue {
syntax: "<number>";
inherits: false;
initial-value: 0;
} That’s the whole registration. Now --hue behaves like a real animatable number.
The Problem Without @property
Without registration, animating a custom property produces a discrete jump — no easing, no interpolation:
/* ❌ This doesn't animate smoothly */
.box {
--color: hsl(0deg 80% 50%);
background: var(--color);
transition: --color 1s ease;
}
.box:hover {
--color: hsl(240deg 80% 50%);
} The transition fires, but since --color is just a string, the browser snaps from one value to the other at 50% through the animation.
@property Syntax
@property --property-name {
syntax: "<type>"; /* required */
inherits: true | false; /* required */
initial-value: ...; /* required if syntax isn't "*" */
} syntax — the value type. Supported descriptors include:
"<number>"— unitless numbers"<integer>"— integers"<length>"—px,em,rem, etc."<percentage>"—%values"<color>"— any CSS color"<angle>"—deg,rad,turn"<length-percentage>"— either a length or a percentage"<transform-list>"— transform functions"*"— any value (no interpolation — same as unregistered)
You can also specify a list of accepted values or combine types with |:
@property --spacing {
syntax: "<length> | <percentage>";
inherits: false;
initial-value: 0px;
} inherits — whether the property inherits down the DOM tree like color does (true) or stays scoped to the element it’s set on (false).
initial-value — the fallback value when the property isn’t set. Required for any typed syntax other than "*".
Animating a Gradient
The classic @property use case — gradients can’t normally be transitioned because they’re background images, not colors. But you can animate the stops by registering the color variables:
@property --from {
syntax: "<color>";
inherits: false;
initial-value: oklch(65% 0.2 30);
}
@property --to {
syntax: "<color>";
inherits: false;
initial-value: oklch(65% 0.2 260);
}
.card {
background: linear-gradient(135deg, var(--from), var(--to));
transition: --from 0.6s ease, --to 0.6s ease;
}
.card:hover {
--from: oklch(75% 0.25 140);
--to: oklch(55% 0.3 300);
} Each color variable is now independently interpolatable. The gradient smoothly morphs on hover — something impossible without @property.
Animating a Hue Rotation
Register a number, then drive everything with it:
@property --hue {
syntax: "<number>";
inherits: false;
initial-value: 0;
}
.icon {
--hue: 0;
color: hsl(var(--hue) 70% 50%);
transition: --hue 0.4s ease-out;
}
.icon:hover {
--hue: 200;
} CSS keyframes with @property
@property works with @keyframes too, enabling full keyframe control over custom properties:
@property --angle {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
@keyframes spin-gradient {
to { --angle: 360deg; }
}
.spinning-border {
background: conic-gradient(
from var(--angle),
oklch(70% 0.3 0),
oklch(70% 0.3 120),
oklch(70% 0.3 240),
oklch(70% 0.3 0)
);
animation: spin-gradient 3s linear infinite;
} Without @property, the --angle variable would snap frame to frame — no smooth conic rotation.
Properties That Can’t Be Animated
@property only solves the custom property problem. Many native CSS properties remain non-animatable because they have no intermediate state to interpolate between:
Layout & display:
display— no in-between state betweennoneandblockfloat,clear,positionoverflow,overflow-x,overflow-yz-index— treated as discrete by most browsers
Backgrounds & borders:
background-image— can’t cross-fade images natively via transitionbackground-repeat,background-attachmentborder-style—solidtodashedhas no interpolable midpointborder-image-source
Text & fonts:
font-family— no meaningful midpoint between two typefacestext-align,vertical-aligntext-transform,white-space
Visibility: visibility has special discrete behavior — it snaps at 0% or 100% of the animation, not at a smooth midpoint. Use it with opacity for fade effects.
Animation and transition props themselves:
animation-*andtransition-*properties can’t be transitioned
For these, your options are JavaScript-driven approaches, keyframe animation with discrete step timing (steps(1)), or restructuring to use an animatable proxy value.
JavaScript Registration
You can also register properties in JavaScript via the Properties and Values API:
if (CSS.registerProperty) {
CSS.registerProperty({
name: "--progress",
syntax: "<number>",
inherits: false,
initialValue: "0",
});
} This is useful for registering properties dynamically or when you need to support environments where @property isn’t parsed (though both have the same browser support).
Progressive Enhancement
@property is well-supported in modern browsers. The safe pattern: set a fallback value, register with @property, and let unsupported browsers fall back gracefully.
/* Fallback for browsers without @property */
.button {
background: oklch(65% 0.2 250);
transition: background 0.3s ease;
}
/* With @property, this overrides with a smooth custom-property animation */
@property --btn-hue {
syntax: "<number>";
inherits: false;
initial-value: 250;
}
@supports (background: oklch(0% 0 0)) {
.button {
background: oklch(65% 0.2 var(--btn-hue));
transition: --btn-hue 0.3s ease;
}
.button:hover {
--btn-hue: 140;
}
} 








