What does registering a custom property with the CSS @property at-rule give you that a plain --name: declaration does not?
answer
- gives a custom property a type
- three descriptors, one at-rule
- initial-value replaces the missing case
- inherits: false is otherwise impossible
- typed values can interpolate
basics
~20 s@property registers a custom property with a syntax type, an inherits flag and an initial-value. That buys type checking, a real fallback instead of an unset value, the ability to opt out of inheritance, and values the browser can interpolate.
solid answer
~60 sAn unregistered custom property has no type: the browser stores whatever tokens you wrote, it always inherits, and it has no initial value. `@property` adds a declaration of intent: ``` @property --accent { syntax: "<color>"; inherits: true; initial-value: black; } ``` That gives four concrete things. First, **type checking** — a value that does not match `syntax` is rejected, so consumers keep seeing a colour instead of a length. Second, a **real initial value**, so a bad or missing assignment falls back to something sensible rather than making the consuming declaration invalid at computed-value time. Third, **control over inheritance**, since `inherits: false` gives you a custom property that does not leak to descendants. Fourth, because the value now has a known type, the browser can **interpolate** between two of them, which is what makes a `<length>`, `<color>` or `<angle>` custom property animatable at all. All three descriptors are required, except `initial-value`, which is optional when `syntax` is the universal `"*"`. Registration is supported in Chrome 85+, Safari 16.4+ and Firefox 128+.
code
css · 14 lines@property --angle {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
.badge {
background: conic-gradient(from var(--angle), #00aaff, #ff00aa, #00aaff);
transition: --angle 600ms linear;
}
.badge:hover {
--angle: 360deg;
}go deeper
Know that @property exists and what its three descriptors are called — syntax, inherits and initial-value — and that a plain --name declaration has none of them.
Explain what each descriptor buys: a type that values are checked against, an explicit initial value, a way to stop inheritance, and a computed type the browser can interpolate between.
Show judgment about when to register — shared tokens where a wrong assignment must fail safely, values that must not inherit, values that must animate — and note that unsupported browsers just keep the property untyped.
Own it as a contract: which cross-team tokens are typed, what their initial values promise to consumers, and how a design that depends on interpolating a registered property degrades where registration is unavailable.
## What an unregistered custom property is When you write `--accent: teal`, the browser does not parse `teal` as a colour. It stores the declaration's value as a stream of tokens, with three fixed characteristics: - **No type.** Any tokens are accepted; validity is only tested when something substitutes the value into a real property. - **Always inherits.** There is no way to make an unregistered custom property stop at one element. - **No initial value.** If nothing declares it, the property is missing — the "guaranteed-invalid" value — which is what the `var()` fallback exists to cover. `@property` lets you declare all three explicitly. ## The at-rule ```css @property --accent { syntax: "<color>"; inherits: true; initial-value: black; } @property --angle { syntax: "<angle>"; inherits: false; initial-value: 0deg; } ``` The prelude is the property name. Three descriptors are defined: `syntax`, `inherits`, and `initial-value`. All are required for the registration to take effect, except that `initial-value` may be omitted when `syntax` is the universal `"*"`, which registers a property that is still untyped but can opt out of inheritance. An incomplete or malformed `@property` rule is simply dropped, and the property stays unregistered — a quiet failure mode worth knowing when a registration "does nothing". The `syntax` string is a small grammar of its own. Common shapes: - a single type: `"<color>"`, `"<length>"`, `"<number>"`, `"<percentage>"`, `"<angle>"`, `"<time>"`, `"<image>"`, `"<integer>"`, `"<length-percentage>"` - alternatives with `|`: `"<length> | auto"` - keyword literals: `"small | medium | large"` - lists: `"<length>+"` (space-separated), `"<color>#"` (comma-separated) - the universal accept-anything: `"*"` ## What registration changes **Type checking at the source.** A declaration whose value does not match the declared `syntax` is rejected for that property. The property then uses its inherited value, or its `initial-value` where there is nothing to inherit. Compare that with the unregistered case, where a wrong value sails through and takes down the *consuming* declaration instead, leaving you debugging a `color` that mysteriously came from a parent. **A guaranteed value.** Because the property has an `initial-value`, `var(--accent)` always resolves to something. Fallback arguments become a convenience rather than a necessity, and the "property is missing" case disappears. **Opt-out of inheritance.** `inherits: false` gives a property that applies only to the element it was declared on. This matters for anything that should be per-element by nature — a local angle, a per-component offset — because an inheriting property silently reaches every descendant that consumes it. **Interpolation.** This is the headline feature. An untyped token stream cannot be interpolated: the browser has no idea that `0deg` and `360deg` are two points on a numeric scale. Registering with `syntax: "<angle>"` gives the value a computed type, and the browser can then compute intermediate values between two of them. That is what makes a custom property usable as an animated input — most visibly for gradients, which are otherwise not animatable at all: ```css @property --angle { syntax: "<angle>"; inherits: false; initial-value: 0deg; } .badge { background: conic-gradient(from var(--angle), #0af, #f0a, #0af); } ``` With `--angle` registered, changing it produces a smooth sweep; unregistered, the value would jump. ## Computed values become typed too Registration also changes what the property's computed value *is*. A registered `<length>` computes to an absolute length, so `--pad: 2em` resolves against the element's font size at computation time and is inherited as the resolved length, not as the literal tokens `2em`. Unregistered, descendants inherit the raw tokens and each one resolves `2em` against its own font size. That difference is subtle and occasionally surprising when adding a registration to an existing token. ## Support and rollout `@property` shipped in Chrome 85, Safari 16.4, and Firefox 128 (2024), so it is broadly available in current browsers but not in older ones. The graceful part is that an unsupported browser ignores the at-rule and keeps treating the property as an ordinary untyped custom property: static values still work, animation of it simply does not happen. If a design depends on the interpolation, that is the piece to plan a fallback for, not the value itself. ## When to use it Register when a custom property has a real type that other people will assign to — shared tokens crossing team boundaries benefit most, because the type check turns a silent mis-typing into a contained one. Register when a property must not inherit. Register when the value has to interpolate. For a one-off local value consumed two lines below where it is declared, plain declarations remain the simpler thing.
- Which @property descriptors are required, and what happens if one is missing?`syntax`, `inherits` and `initial-value` are all required, except that `initial-value` may be omitted when `syntax` is the universal `"*"`. An `@property` rule missing a required descriptor is invalid and dropped entirely, so the property silently stays unregistered — untyped, always inheriting, with no initial value. That quiet failure is the usual reason a registration appears to do nothing.
- What happens when a value that does not match the declared syntax is assigned to a registered property?The declaration is rejected for that property rather than being stored. The property then computes to its inherited value if it inherits, or to its `initial-value` otherwise. That is the key improvement over unregistered properties, where a wrong value is stored happily and instead breaks whichever declaration consumes it, at computed-value time.
- Does registering an existing custom property change how its value is inherited?It can. A registered property computes to a typed value before inheriting, so a `<length>` declared as `2em` resolves against the declaring element's font size and descendants inherit that absolute length. Unregistered, descendants inherit the literal tokens and each resolves `2em` against its own font size. Adding a registration to a long-standing relative token can therefore shift rendering.
saying these in an interview costs you the question
- Thinks unregistered custom properties can animate smoothly
- Says @property is only about documentation or tooling
- Believes inherits is optional or defaults to false
- Assumes a bad value on a registered property still gets stored
- Treats @property as universally supported since forever