CSS @property: Typed Custom Properties Explained
@property registers a CSS custom property with a type, initial value, and inheritance rule — unlocking smooth animation and real error checking.
@property is a CSS at-rule that lets you register a custom property with an explicit syntax type, an initial value, and an inheritance rule — turning a plain, untyped --variable into something the browser actually understands and can animate. Without it, custom properties are just strings: the browser has no idea --angle is supposed to be an angle or --brand-color is supposed to be a color, so it can’t interpolate between two values during a transition.
The problem with plain custom properties
A standard custom property is defined with nothing more than a name and a value:
:root {
--angle: 0deg;
}
To the CSS engine, --angle is just a token stream — it doesn’t know 0deg is an angle rather than an arbitrary string. That has a concrete consequence: if you try to transition --angle from 0deg to 360deg to spin a conic gradient, nothing animates. The browser can’t interpolate between two values it doesn’t recognize as numbers, so it just swaps the value at the halfway point of the transition, producing a jump instead of a spin.
Registering a typed property
@property fixes this by declaring the property’s type up front:
@property --angle {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
.dial {
background: conic-gradient(from var(--angle), tomato, gold);
transition: --angle 0.6s ease;
}
.dial:hover {
--angle: 360deg;
}
Three fields are required:
syntax— the value type, such as"<color>","<length>","<number>","<percentage>", or"<angle>". This is what enables interpolation.inherits— whether elements inherit the property from their parent, exactly like theinheritsbehavior built into standard CSS properties.initial-value— the fallback used if no value is set, similar to howcolordefaults tocanvastext.
Once registered, the browser type-checks every assignment. A value that doesn’t match the declared syntax is rejected and falls back to the initial value instead of silently propagating a broken string through your gradients.
What this unlocks
- Animatable gradients and angles. Rotating conic gradients, sweeping color stops, and other effects that were previously JavaScript-only become pure CSS.
- Type safety. An invalid assignment — say, a plain string where a
<color>is expected — is caught and discarded instead of quietly breaking downstream calculations. - Predictable inheritance.
inherits: falsestops a property from leaking into child elements the way an ordinary custom property would, which is useful for component-scoped tokens.
@property vs a plain custom property
Plain --custom-property | @property-registered | |
|---|---|---|
| Type checking | None — any token stream is valid | Enforced against syntax |
| Animatable | Only via discrete jumps | Smoothly interpolated if the syntax allows it |
| Inheritance | Always inherits | Configurable via inherits |
| Initial value | Empty/invalid by default | Explicit initial-value |
| Invalid values | Break silently | Rejected, fall back to initial value |
Falling back gracefully
Because @property is a progressive enhancement, it’s worth checking support with @supports before relying on it for anything critical:
@supports (background: paint(something)) or (color: color-mix(in srgb, red, blue)) {
@property --brand-hue {
syntax: "<number>";
inherits: false;
initial-value: 220;
}
}
In practice, browsers that don’t understand @property simply ignore the at-rule and treat the referenced custom property as a plain, untyped one — the gradient or color still renders, it just won’t animate smoothly. That makes @property a safe addition to an existing stylesheet: worst case, an unsupported browser falls back to the same jump-cut behavior it already had.
A second example: typed color transitions
Typed properties aren’t limited to angles. A registered <color> property lets the browser interpolate between two colors properly, in the color space it understands, rather than treating the value as an opaque string:
@property --accent {
syntax: "<color>";
inherits: true;
initial-value: #6366f1;
}
.button {
background: var(--accent);
transition: --accent 0.3s ease;
}
.button:hover {
--accent: #ec4899;
}
Without registering --accent, hovering would swap the background color instantly at the midpoint of the transition instead of fading between the two.
Where it fits with modern CSS
@property pairs naturally with other CSS features that lean on custom properties as design tokens, like CSS cascade layers for organizing where those tokens are defined, or clamp()-based fluid typography where a typed <length> property can be animated on interaction. It’s also a natural companion to the View Transitions API, since transitions there rely on the same interpolation machinery — a typed custom property can drive a smooth cross-page animation the same way it drives a hover effect. And because @property behaves like a real CSS feature rather than a build-time convenience, it composes cleanly with container queries: a component can expose a typed property that responds to its container’s size without any JavaScript wiring it up.
Syntax strings beyond the basics
The syntax descriptor accepts more than single types. A pipe-separated list restricts the property to one of several alternatives, and a bare * accepts any value — effectively opting back into the untyped behavior of a plain custom property while still getting to set inherits and initial-value explicitly:
@property --gap-size {
syntax: "<length> | <percentage>";
inherits: false;
initial-value: 1rem;
}
This is useful for properties that legitimately need to accept more than one unit type — a gap that’s sometimes a fixed length and sometimes a percentage of its container, for instance — without giving up type checking entirely for values outside that set.
The takeaway
@property upgrades a custom property from an untyped string to a real, type-checked CSS value with a defined initial value and inheritance behavior. The immediate payoff is animation — gradients, angles, and other values that used to require JavaScript can now transition natively — but the type checking alone is worth it for catching invalid values before they silently break your layout.
Tagged
Keep reading
Takina · · 4 min read CSS object-fit and object-position, Explained
object-fit controls how an image or video is cropped inside its box, and object-position controls which part of it stays visible. How they work together.
Takina · · 5 min read CSS inherit, initial, unset & revert Explained
CSS's four global keywords control where a property's value comes from. How inherit, initial, unset, and revert differ, with a comparison table.
Takina · · 4 min read Dynamic Viewport Units: dvh, svh, and lvh Explained
dvh, svh, and lvh fix the classic mobile vh bug where browser toolbars cut off full-height layouts. Here's what each unit measures and when to use it.