The @property at-rule gives CSS custom properties something they never had: a type system. You can tell the browser that a variable is a color, an angle, or a length, and suddenly things like gradient transitions and type-safe fallbacks just work.
I started using @property after spending too long faking gradient animations with pseudo-elements and opacity tricks. Once I registered the color stops as typed properties, the browser handled the interpolation natively. No JavaScript, no hacks.
What Is @property?
Regular CSS custom properties (variables) are treated as strings by the browser. It stores --brand-color: #3490dc as a raw text value. This means the browser can’t interpolate between two values during a transition because it doesn’t know what type of data it’s holding.
@property solves this by letting you register a custom property with three pieces of information:
- syntax – the data type (
<color>,<angle>,<length>,<number>, etc.) - initial-value – a default fallback
- inherits – whether child elements inherit the value
@property --brand-color {
syntax: "<color>";
inherits: true;
initial-value: #3490dc;
}All three descriptors are required (except initial-value when syntax is "*").
The Syntax Descriptor
The syntax descriptor defines what values the property accepts. Here are the supported types:
| Syntax | Accepts | Example |
|---|---|---|
<color> | Any valid color | #ff0, rgb(0 0 0), hsl(120 50% 50%) |
<length> | Lengths with units | 16px, 2rem, 50vw |
<number> | Unitless numbers | 0, 1.5, 42 |
<percentage> | Percentage values | 50%, 100% |
<angle> | Angle values | 45deg, 0.5turn, 3.14rad |
<integer> | Whole numbers only | 0, 3, -1 |
<time> | Duration values | 300ms, 1.5s |
<image> | Image references/gradients | url(...), linear-gradient(...) |
<transform-function> | Transform functions | rotate(45deg), scale(1.2) |
"*" | Any value (universal) | Any valid CSS value |
You can also combine types with | for alternatives: "<color> | <length>". Or use + and # for space-separated and comma-separated lists.
Why It Matters: Animating the Impossible
This is the feature that makes @property worth learning. Without it, the browser can’t transition gradients because it sees them as opaque strings. With typed properties, it can interpolate each color stop individually.
Hover over both cards below to see the difference:
The left card uses a regular transition: background. On hover, the gradient snaps instantly because the browser can’t interpolate between two gradient strings. The right card registers each color stop with @property as a <color>, so the browser transitions them smoothly.
Here’s the code for the smooth version:
@property --grad-start {
syntax: "<color>";
inherits: false;
initial-value: #3490dc;
}
@property --grad-end {
syntax: "<color>";
inherits: false;
initial-value: #6574cd;
}
.card {
background: linear-gradient(135deg, var(--grad-start), var(--grad-end));
transition: --grad-start 0.8s ease, --grad-end 0.8s ease;
}
.card:hover {
--grad-start: #e3342f;
--grad-end: #ffb636;
}The key insight: you transition the custom property itself, not the background. The browser knows the property is a <color>, so it interpolates between the two color values. The gradient rebuilds on every frame with the interpolated colors.
Animating Angles: Rotating Gradients
The same principle works for angles. Register a property as <angle> and you can animate it in a @keyframes rule. This example creates a continuously rotating conic gradient:
rotating via
@property
@property --rotate-angle {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
.spinner {
background: conic-gradient(
from var(--rotate-angle),
#AE0E38, #E57622, #ffb636,
#3490dc, #6574cd, #AE0E38
);
animation: spin 4s linear infinite;
}
@keyframes spin {
to { --rotate-angle: 360deg; }
}Before
@property, rotating a conic gradient required JavaScript or rotating the entire element withtransform: rotate(). With a registered<angle>property, you animate just the gradient’s starting angle.
Animated Hue Cycling
By registering a custom property as <number>, you can animate it and feed it into hsl(). This creates a color wave effect where each cell cycles through hues at a different offset:
@property --hue {
syntax: "<number>";
inherits: false;
initial-value: 220;
}
.cell {
background: hsl(var(--hue), 65%, 55%);
animation: hue-cycle 6s ease-in-out infinite alternate;
}
.cell:nth-child(2) { animation-delay: -0.5s; }
.cell:nth-child(3) { animation-delay: -1s; }
/* ... staggered delays for each cell */
@keyframes hue-cycle {
0% { --hue: 220; }
50% { --hue: 340; }
100% { --hue: 40; }
}Type Validation and Safe Fallbacks
With regular custom properties, assigning an invalid value means the property resolves to nothing, and the declaration using it is thrown out entirely. The browser falls through to the inherited or initial value of the native property – which is often not what you want.
@property changes this. If you assign an invalid value to a registered property, the browser falls back to the initial-value you defined instead:
(falls back to initial-value)
@property --safe-size {
syntax: "<length>";
inherits: false;
initial-value: 16px;
}
.element {
--safe-size: banana; /* invalid for <length> */
font-size: var(--safe-size); /* uses 16px, not broken */
}Controlling Inheritance
Regular custom properties always inherit from parent to child. With @property, setting inherits: false stops the value from leaking down the DOM tree. This is useful for scoped design tokens that shouldn’t bleed into nested components.
@property --scoped-accent {
syntax: "<color>";
inherits: false;
initial-value: #aeafb4;
}
.parent {
--scoped-accent: #75bbe8; /* only applies to .parent */
border-color: var(--scoped-accent); /* blue */
}
.child {
border-color: var(--scoped-accent); /* gray - initial-value */
}Animated Borders
Combining @property with conic-gradient gives you animated gradient borders – no pseudo-elements needed. The trick: use a thin outer wrapper with the animated gradient background and an inner element with a solid background, leaving only a 2px gap for the border effect.
Animated Border
The gradient rotates around the card using a single @property angle variable.
@property --border-angle {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
.card-wrapper {
padding: 2px;
border-radius: 10px;
background: conic-gradient(
from var(--border-angle),
#AE0E38, #E57622, #ffb636, #3490dc, #AE0E38
);
animation: border-spin 3s linear infinite;
}
.card-inner {
background: var(--light-bg);
border-radius: 8px;
padding: 24px;
}
@keyframes border-spin {
to { --border-angle: 360deg; }
}This technique works well for gradient borders that need motion.
Animated Progress Bar
Register a <percentage> property and you can animate width smoothly with @keyframes:
@property --progress {
syntax: "<percentage>";
inherits: false;
initial-value: 0%;
}
.progress-fill {
width: var(--progress);
animation: fill 3s ease-in-out infinite alternate;
}
@keyframes fill {
0% { --progress: 5%; }
100% { --progress: 92%; }
}@property vs CSS.registerProperty()
You can also register properties from JavaScript using CSS.registerProperty(). The functionality is identical, but there are trade-offs:
| Feature | @property (CSS) | CSS.registerProperty() (JS) |
|---|---|---|
| Declared in | CSS file | JavaScript |
| Render-blocking | No (parsed with CSS) | Yes (runs in JS) |
| Can re-register | Last definition wins | Throws if already registered |
| SSR friendly | Yes | No (needs DOM) |
CSS.registerProperty({
name: '--brand-color',
syntax: '<color>',
inherits: true,
initialValue: '#3490dc'
});Prefer the CSS @property at-rule over CSS.registerProperty() unless you need to register properties dynamically at runtime. The CSS approach is parsed alongside your stylesheets and doesn’t require JavaScript to execute first.
Browser Support
@property reached universal browser support in July 2024:
- Chrome 85+ (July 2020)
- Edge 85+ (July 2020)
- Safari 16.4+ (March 2023)
- Firefox 128+ (July 2024)
Firefox was the last holdout. With version 128, the feature is now available everywhere. You can use @supports to provide a fallback for older browsers:
@supports (background: paint(id)) {
/* @property is supported */
}
/* Or feature-test a registered property */
@supports (--test: initial) {
/* basic custom properties supported */
}FAQs
Common questions about @property:
@property and regular CSS variables?
--name: value) are untyped strings. The browser stores and passes them as-is. @property registers a variable with a specific type (like <color> or <angle>), an initial fallback value, and an inheritance flag. This gives the browser enough information to animate the property, validate assigned values, and fall back gracefully when something invalid is assigned.@property?
@property with syntax: "<color>", use them in the gradient via var(), then transition or animate the custom properties directly. The browser interpolates between the color values, producing a smooth gradient transition. Without @property, gradients snap instantly because the browser sees them as opaque strings.@property descriptors required?
syntax and inherits are always required. initial-value is required unless the syntax is "*" (the universal syntax). If you omit a required descriptor, the entire @property rule is invalid and the browser ignores it.initial-value defined in the @property rule. This is different from regular custom properties, where an invalid value causes the declaration to be discarded entirely and the property resolves to its inherited or initial value. Registered properties give you a reliable safety net.@property or CSS.registerProperty()?
@property at-rule in most cases. It's parsed with the stylesheet, doesn't require JavaScript, and doesn't block rendering. Use CSS.registerProperty() only when you need to register properties dynamically at runtime, for example based on user preferences or A/B test configuration.@property work in all browsers?
Summary
@property fills a real gap in CSS. Before it shipped, custom properties were just string containers. Now they carry type information that unlocks gradient animations, safe fallbacks, and scoped inheritance – all in pure CSS.
If you’re already using CSS variables, @property is the next step. Start with the gradient transition trick since that’s the most visible win, then explore typed fallbacks for your design tokens.

