typed Custom Properties, animatable and validated
CSS Custom Properties are powerful but untyped: the browser treats them as plain strings. CSS @property changes that fundamentally. With a type declaration, inheritance control and an initial value, Custom Properties become full, animatable CSS values. Gradients, colors and numbers can then be interpolated and hooked into transitions.
Table of Contents
- 1. What @property solves
- 2. The syntax descriptor: types for Custom Properties
- 3. inherits: controlling inheritance
- 4. initial-value: safe fallback values
- 5. Animatability: interpolating Custom Properties
- 6. Animating gradients with @property
- 7. Number properties for CSS counters and layout
- 8. CSS.registerProperty() in JavaScript
- 9. @property vs. untyped Custom Properties
- 10. Summary
- 11. FAQ
1. What @property solves
CSS Custom Properties, better known as CSS variables, have been available in every browser since 2017 and have revolutionized the styling of design systems. They do have one fundamental limitation, though: the browser treats them as typeless strings. That means transitions and animations on Custom Properties simply do not work, because the browser has no way of interpolating between --color: #ff0000 and --color: #0000ff when it does not know these are color values in the first place. CSS @property gives the browser that information explicitly.
The @property rule is part of the CSS Houdini APIs, a collection of low-level browser APIs that give developers access to the core of the CSS engine. @property is the most accessible and practical feature of that set, and today it works in every modern browser without any experimental flags. Beyond animatability, @property also solves the problem of validation: the browser rejects invalid values and falls back to the declared initial-value instead of letting a broken string slip through. That makes design token systems considerably more robust.
A third benefit of @property concerns inheritance control. Regular Custom Properties are always inherited in the DOM: a value set on body applies to every child element unless it is explicitly overridden. With inherits: false in an @property declaration, that behavior can be switched off. This is particularly useful for animation values that should stay scoped to a single component and must not be influenced accidentally through a parent selector.
2. The syntax descriptor: types for Custom Properties
The syntax descriptor in an @property declaration defines the type of the Custom Property. Supported types include <color>, <length>, <number>, <percentage>, <angle>, <time>, <resolution>, <transform-list>, <custom-ident> and <image>. Combinations are possible using spaces for sequences and | for alternatives. The universal type * matches the untyped behavior of ordinary Custom Properties and disables validation entirely.
The syntax type determines how the browser interprets the value and whether interpolation is possible at all. Only types that represent numeric or color values can be interpolated, namely <color>, <length>, <number>, <percentage>, <angle> and combined types such as <length-percentage>. String types like <custom-ident> cannot be interpolated but still benefit from validation. Declaring a precise type in @property is the decisive step that turns Custom Properties from string placeholders into genuine CSS values.
/* Typed Custom Properties with @property */
/* Color type: enables color interpolation in transitions */
@property --brand-color {
syntax: '<color>';
inherits: false;
initial-value: #7c3aed;
}
/* Number type: enables numeric interpolation */
@property --progress {
syntax: '<number>';
inherits: false;
initial-value: 0;
}
/* Percentage type: for layout-related animations */
@property --fill-level {
syntax: '<percentage>';
inherits: true;
initial-value: 0%;
}
/* Angle type: for rotation animations */
@property --rotation {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
/* Length: for custom spacing animations */
@property --offset-x {
syntax: '<length>';
inherits: false;
initial-value: 0px;
}
/* Usage: transitions now work on the custom property */
.card {
background-color: var(--brand-color);
transition: --brand-color 0.4s ease;
}
.card:hover {
--brand-color: #4a1d96;
}
3. inherits: controlling inheritance
The inherits field in an @property declaration is a boolean value (true or false) and determines whether the value of the Custom Property is inherited down the DOM tree. Regular CSS Custom Properties always inherit. With @property and inherits: false, the Custom Property becomes local: it only applies to the element it was set on directly, not to that element's children.
In practice, inherits: false is especially useful for animation and transition values that should stay bound to a specific component. If, for example, a hover animation is implemented with a --hover-progress property, that property should not leak from parent elements down to children, since doing so could trigger unwanted animation effects. inherits: true, on the other hand, suits design tokens such as brand colors that need to stay consistent across an entire subtree.
4. initial-value: safe fallback values
The initial-value descriptor defines the default value of the Custom Property, used whenever the property has not been explicitly set or an invalid value was supplied. This is a fundamental improvement over regular Custom Properties, which return an empty string when no value is present and can therefore produce invalid CSS declarations. With @property and a declared initial-value, the Custom Property is always in a valid state.
The initial-value must be compatible with the declared syntax type: a <color> property cannot have 0px as its initial value. The browser validates this consistency while parsing the @property declaration. If validation fails, the entire @property declaration is discarded and the property behaves like a regular, untyped Custom Property instead. That makes the declaration resilient against typos and considerably simplifies debugging in complex design systems.
5. Animatability: interpolating Custom Properties
The single most practical benefit of CSS @property is the animatability of typed Custom Properties. Without @property, the browser treats Custom Properties as strings and cannot interpolate between two values: a transition on --my-value jumps straight to the target value instead of moving smoothly. With a typed @property declaration, this changes fundamentally: the browser knows the type and can perform numeric interpolation.
That opens up entirely new possibilities in CSS animation. Instead of defining a transition directly on transform: rotate(45deg), the rotation amount can be stored in an <angle>-typed Custom Property, and that property itself becomes the thing you animate. A single CSS class change can then update the animatable value, and the transition fires automatically. For progress indicators, fill-level displays and color transitions, @property is therefore the most elegant CSS solution available, with no JavaScript animation library required.
/*
* Animatable gradient using @property
* Without @property: gradients cannot be transitioned
* With @property: each color stop can be interpolated
*/
@property --gradient-start {
syntax: '<color>';
inherits: false;
initial-value: #7c3aed;
}
@property --gradient-end {
syntax: '<color>';
inherits: false;
initial-value: #4a1d96;
}
@property --progress-value {
syntax: '<percentage>';
inherits: false;
initial-value: 0%;
}
/* Hero with animatable gradient */
.hero-gradient {
background: linear-gradient(
135deg,
var(--gradient-start),
var(--gradient-end)
);
transition:
--gradient-start 0.6s ease,
--gradient-end 0.6s ease;
}
.hero-gradient:hover {
--gradient-start: #c4b5fd;
--gradient-end: #7c3aed;
}
/* Progress bar with @property animation */
.progress-bar {
--progress-value: 0%;
width: var(--progress-value);
background: linear-gradient(90deg, #7c3aed, #c4b5fd);
height: 6px;
border-radius: 3px;
transition: --progress-value 0.5s cubic-bezier(0.22, 1, 0.36, 1);
}
/* Keyframe animation on a custom property */
@keyframes spin-dial {
from { --rotation: 0deg; }
to { --rotation: 360deg; }
}
.dial {
transform: rotate(var(--rotation));
animation: spin-dial 2s linear infinite;
}
6. Animating gradients with @property
The most common and most impressive use case for CSS @property is gradient animation. In plain CSS, gradients cannot be animated with transitions, because the browser treats the color stops as strings and cannot interpolate them. With @property, the color values inside a gradient are defined as typed <color> properties instead. The gradient itself stays the same function it always was, but its inputs, the Custom Properties, can now transition smoothly.
This pattern is especially valuable for modern web UIs: hero sections that shift color on hover, buttons with animated color gradients, loading bars with dynamic color, and progress indicators that adjust their gradient based on a percentage value. All of this can be built with @property in plain CSS. In the past, effects like these required JavaScript libraries such as GSAP updating the DOM on every animation frame. @property hands that job back to the browser entirely.
7. Number properties for CSS counters and layout
Alongside colors and gradients, <number>-typed Custom Properties are another powerful use case. A common pattern is the animated counter: an @property declaration with syntax: '<integer>' and a starting value of 0. A @keyframes animation that moves the property from 0 to its target value, combined with a counter() function inside content to display the number, produces a purely CSS-based animated numeric counter. No JavaScript, no DOM manipulation, no requestAnimationFrame loop.
<number> properties are also well suited as inputs for custom easing curves feeding complex calc() expressions. A state machine expressed in CSS that animates between 0 and 1 and proportionally drives several other properties is elegantly achievable with @property. Combined with @keyframes and animation-timeline, this produces animation systems that stay entirely within CSS and require no JavaScript coordination.
/* CSS counter animation using @property <integer> */
@property --count {
syntax: '<integer>';
inherits: false;
initial-value: 0;
}
.stat-counter {
counter-reset: count var(--count);
animation: count-up 2s cubic-bezier(0.22, 1, 0.36, 1) both;
animation-delay: 0.3s;
}
.stat-counter::after {
content: counter(count);
font-variant-numeric: tabular-nums;
font-feature-settings: "tnum";
}
/* Animate to the target value via CSS custom property */
@keyframes count-up {
from { --count: 0; }
to { --count: 1247; } /* Target number */
}
/* Staggered counting for multiple stats */
.stats-grid .stat:nth-child(1) { animation-delay: 0.1s; }
.stats-grid .stat:nth-child(2) { animation-delay: 0.2s; }
.stats-grid .stat:nth-child(3) { animation-delay: 0.3s; }
/* Number-driven layout animation */
@property --panel-weight {
syntax: '<number>';
inherits: false;
initial-value: 1;
}
.panel {
flex: var(--panel-weight);
transition: --panel-weight 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.panel:focus-within {
--panel-weight: 2.5;
}
8. CSS.registerProperty() in JavaScript
CSS @property has a JavaScript equivalent: CSS.registerProperty(). This API accomplishes the same task as the CSS rule but can be called from JavaScript. That is especially relevant for web components and JavaScript frameworks where CSS registrations need to happen programmatically. The signature reads CSS.registerProperty({ name: '--property-name', syntax: '<color>', inherits: false, initialValue: '#000' }). Property names, syntax strings and the underlying semantic rules are identical to the CSS variant.
One important difference between @property and CSS.registerProperty(): the CSS variant applies to every element in the document as soon as the stylesheet loads. CSS.registerProperty() must be called before the property is used for the first time. Registering a property that already exists throws an InvalidModificationError. In real-world web component implementations, it is common practice to first check whether the property has already been registered before calling registerProperty(). Registration is global and cannot be undone.
9. @property vs. untyped Custom Properties
A direct comparison shows when CSS @property is genuinely necessary and when regular Custom Properties are enough. For purely static design tokens used as strings, such as font families or shadow definitions, @property is not required. As soon as animations, transitions or validation enter the picture, @property is the only native CSS solution available.
| Property | Custom Property (--var) | @property | Relevance |
|---|---|---|---|
| Type validation | No type, any string | Typed, invalid values fall back to initial-value | Design systems, tokens |
| Transitions | Not interpolatable | Fully animatable | Color transitions, fades |
| Inheritance | Always inherited | Controllable (inherits: false) | Animation scope |
| Fallback value | Empty string or var(--x, fallback) | initial-value always valid | Robustness, debugging |
| Browser support | All browsers since 2017 | Chrome 85+, FF 128+, Safari 16.4+ | Progressive enhancement |
Browser support for @property improved considerably in 2024. Firefox 128 and Safari 16.4 shipped full support, which means @property is now available in over 88% of browser versions in use. The recommended approach is progressive enhancement: declare @property, but write the surrounding code so it still works correctly without interpolation. The effect improves for users whose browser supports it, while everyone else gets a functional, simply non-animated fallback.
Mironsoft
Modern CSS, design token systems and Hyva frontend engineering
Building typed CSS animation systems?
We implement @property-based design token systems, animatable gradients and robust Custom Property architectures for your Hyva and Tailwind projects.
Token system
Introduce @property-based design tokens with validation and an initial value
Animations
Make gradients, colors and numbers animatable with @property
Hyva / Tailwind
Integrate @property into Tailwind v4.0 and Hyva themes
10. Summary
CSS @property is the Houdini feature that turns CSS Custom Properties from untyped strings into full, animatable CSS values. The three descriptors, syntax, inherits and initial-value, give the browser the information it needs for interpolation, validation and correct inheritance. Gradients, colors, numbers and angles can then be hooked into CSS transitions and @keyframes animations, with no JavaScript and no libraries required.
The practical use cases are wide-ranging: animated gradient heroes, progress indicators, purely CSS-based counter animations, robust design token systems with type validation, and component-local animation properties using inherits: false. Browser support has been complete across all modern browsers since 2024, which makes @property production ready today. CSS.registerProperty() offers the same functionality from JavaScript for web components and dynamic registration. No modern CSS animation system should ignore @property.
CSS @property: the essentials at a glance
syntax descriptor
Types: <color>, <length>, <number>, <percentage>, <angle>. Only numeric and color types are interpolatable.
initial-value
Required for non-inheriting properties. A valid fallback when the value is invalid. Must be compatible with syntax.
Animatability
Interpolate gradients, colors and numbers via transition and @keyframes. No JavaScript needed.
registerProperty()
CSS.registerProperty(), the JavaScript equivalent for web components. Call before first use.
11. FAQ: CSS @property
1What is CSS @property?
2Why aren't Custom Properties animatable?
3Which syntax types exist?
4inherits: false, what does it mean?
5What is initial-value for?
6Animating gradients?
7CSS.registerProperty()?
CSS.registerProperty({ name, syntax, inherits, initialValue }). Call before first use. Useful for web components.