@property in Practice: Typed, Animatable CSS Variables
@property upgrades a CSS custom property from an untyped token stream to a typed, validatable, interpolable value — and the rules are stricter than they look.
A custom property written as --brand: #c0ffee looks like a variable, but the value is a token stream the browser is not allowed to interpret. The CSS Variables specification states the consequence plainly: custom properties can be animated, but since the UA has no way to interpret their contents, they always use the "flips at 50%" behavior that is used for any other pair of values that can't be intelligently interpolated[7]. Gradients that jump instead of fade, fallbacks that silently vanish, tokens that "work" until someone passes a number — all of it traces back to that decision.
What Registration Actually Changes
The CSS Properties and Values API extends the variables module, allowing the registration of properties that have a value type, an initial value, and a defined inheritance behaviour, via two methods: a JavaScript API, the registerProperty() method, and a CSS at-rule, the @property rule[6]. MDN describes the author's-side effect: the @property at-rule is used to explicitly define CSS custom properties, allowing for property type checking and constraining, setting default values, and defining whether a custom property can inherit values or not[1]. A name that held text becomes a name that holds a typed value the engine can validate, default, and interpolate.
Three Descriptors, Two Required
A registration is described by three descriptors, and the validity rules are stricter than most CSS authors expect. The syntax descriptor defines the allowed value types for the registered custom property, and it is a required descriptor; if missing or invalid, the entire @property rule is invalid and ignored[2]. The inherits descriptor controls whether or not the registered custom property inherits by default, and it too is required[3]. The initial-value descriptor specifies the initial value for the registered custom property, and it is required unless the syntax descriptor value is the universal syntax[4]. Omit a mandatory descriptor and the rule disappears silently.
Syntax Strings Are a Small Grammar
The syntax value is a quoted string built from a deliberately small vocabulary. It accepts data type names written with angle brackets such as <color> or <length>, bare keywords such as auto or none, or the universal syntax *, which accepts any valid CSS value and cannot be multiplied or combined with other syntax components[2]. Those components can be used alone or multiplied and combined in different ways: the + and # multipliers indicate a space-separated or comma-separated list is expected, so <color># means a comma-separated list of <color> values is the expected syntax, while the vertical line creates "or" conditions between alternatives[2].
Initial Values Must Stand Alone
The initial-value rule has a subtlety that catches people writing design tokens: the value has to be computationally independent. 10px is computationally independent — it doesn't change when converted to a computed value, and 2in is likewise valid because 1in is always equivalent to 96px[4]. However, 3em is not valid, because the value of an em is dependent on the parent's font size[4].
Parse Time Versus Computed-Value Time
The most useful mental model is that registration changes nothing at parse time and everything later. Registering a custom property must not affect the cascade in any way; regardless of what syntax is specified for a registered property, at parse time it is still parsed as normal for a custom property, accepting nearly anything[6]. The check happens at computed-value time instead: if the specified value for a registered custom property violates the registered syntax, the property becomes invalid at computed-value time and thus resets to the registered initial value[6].
That reset is the real payoff. An unregistered custom property has the guaranteed-invalid value as its initial value, and if that value ever appears in a property value, then at computed value time that property becomes invalid at computed-value time[7]. Register --colorPrimary as a color with an initial value of magenta and assign it 23, and the parser falls back to magenta instead of discarding the declaration — which allows for true fallbacks and testing within CSS[9]. web.dev frames the same win as fallback values: no more disappearing styles, because if an invalid value is assigned later, the browser gracefully uses your defined fallback[8].
What Interpolation Buys You
Typing a property is what makes it animatable. When referenced by animations and transitions, custom property values interpolate by computed value, in accordance with the type that they parsed as[6]. Unregistered properties cannot do this: there's no way to smoothly animate (or interpolate) between gradient values, as each gradient declaration is parsed as a string[9]. MDN's guide draws the same line — registration is more robust than the basic cascading variable declaration for transitioning and animating values, because browsers can interpolate between custom values of this type, whereas two-dash properties behave more like a string substitution[12].
@property --progress {
syntax: "<percentage>";
inherits: false;
initial-value: 25%;
}
.bar {
background: linear-gradient(
to right,
#00d230 var(--progress),
black var(--progress)
);
animation: progressAnimation 2.5s ease infinite;
}
@keyframes progressAnimation {
to { --progress: 100%; }
}The equivalent registration from JavaScript is a single call, and it is the older of the two entry points[6]. The CSS.registerProperty() static method registers custom properties, allowing for property type checking, default values, and properties that do or do not inherit their value[5]. Its name, syntax, and inherits members are effectively the same information as the descriptors above.
Registration Is Global, Not Tree-Scoped
Here is the constraint that surprises component authors: property registrations are not scoped to a tree scope, and all registrations, whether they appear in the outermost document or within a shadow tree, interact in a single global registration map for the Document[6]. The spec's reasoning is that elements can exist in multiple tree scopes at the same time, so there is no unambiguous way to decide which tree's registration should parse a given value[6].
Two Ways In, One Precedence Order
Because registration is global, collisions need a rule. If multiple valid @property rules are defined using the same name, the last one in stylesheet order "wins", and if custom properties are registered with the same name using @property in CSS and CSS.registerProperty() in JavaScript, the JavaScript registration wins[1]. The entry points are not symmetric in another way either: calling registerProperty() for a name that has already been registered throws an InvalidModificationError DOMException[5], while redeclaring the same name in CSS is silently allowed.
The Support Window
Registration is no longer a progressive-enhancement gamble. The feature is now available in all three major browser engines and became Baseline Newly available as of July 9, 2024[8]. The web-features explorer records the same date, lists Firefox 128 as the engine that completed the set, and projects the feature as expected to be Widely Available on 2027-01-09[10]. For version floors, browser-compat-data lists Firefox 128 and Safari 16.4 for the at-rule and its descriptors[11].
Where to Use It
The strongest use is not animation — it is turning a design token into a contract. Register spacing, radii, and durations with their real types, and invalid values degrade to a documented default instead of breaking a declaration; the fallback behavior is the type safety, not a bonus feature[8]. Where interpolation is the goal, register the numeric knob rather than the composed result: a <percentage> stop position interpolates, a gradient string never will[6][9]. Treat every registration as document-global API surface, because that is exactly what it is[6].
Sources
[1] https://developer.mozilla.org/en-US/docs/Web/CSS/@property — MDN — @property CSS at-rule
[2] https://developer.mozilla.org/en-US/docs/Web/CSS/@property/syntax — MDN — @property syntax descriptor
[3] https://developer.mozilla.org/en-US/docs/Web/CSS/@property/inherits — MDN — @property inherits descriptor
[4] https://developer.mozilla.org/en-US/docs/Web/CSS/@property/initial-value — MDN — @property initial-value descriptor
[5] https://developer.mozilla.org/en-US/docs/Web/API/CSS/registerProperty_static — MDN — CSS.registerProperty() static method
[6] https://drafts.css-houdini.org/css-properties-values-api-1 — CSS Properties and Values API Level 1 (Editor's Draft)
[7] https://drafts.csswg.org/css-variables-1 — CSS Custom Properties for Cascading Variables Module Level 1
[8] https://web.dev/blog/at-property-baseline — web.dev — @property: Next-gen CSS variables now with universal browser support
[9] https://web.dev/articles/at-property — web.dev — @property: giving superpowers to CSS variables
[10] https://web-platform-dx.github.io/web-features-explorer/features/registered-custom-properties — Web platform features explorer — Registered custom properties
[11] https://github.com/mdn/browser-compat-data/blob/main/css/at-rules/property.json — mdn/browser-compat-data — css/at-rules/property.json
[12] https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Properties_and_values_API — MDN — CSS properties and values API guide