Inheritance behavior and registration with @property in detail
An unregistered custom property always inherits in CSS, regardless of whether its name suggests a local, component-scoped value. Only @property gives a custom property a fixed type, a guaranteed initial value, and explicit inheritance behavior, closing an entire class of bugs that are otherwise hard to track down in large design systems.
Table of Contents
- 1. How custom properties inherit by default, without registration
- 2. @property basics: syntax, inherits, and initial-value
- 3. inherits: false in detail: encapsulating component-local values
- 4. A common bug: unexpected leaking of unregistered properties
- 5. invalid-at-computed-value-time: the fallback difference for invalid values
- 6. A registration strategy for a consistent design system
- 7. CSS.registerProperty(): the same registration at runtime through JavaScript
- 8. Performance effect: registered custom properties and animation
- 9. Browser support and migrating existing custom properties
- 10. Summary
- 11. FAQ
1. How custom properties inherit by default, without registration
A custom property declared simply as --color: blue; does not behave like a normal CSS property in the cascade, it behaves like a plain text token that always inherits, exactly the way color does. That holds even when the property's name suggests a component-scoped purpose, for example --card-spacing: without explicit registration, that value leaks into every nested child element, unless a new value for the same property gets set there.
This default behavior surprises many developers coming from object-oriented languages who expect a variable defined locally on a component to automatically stay local. CSS has no such encapsulation without @property: every custom property is inherently global in cascade terms and inherits to all descendants, until it gets overridden somewhere or reset with unset.
2. @property basics: syntax, inherits, and initial-value
The @property rule registers a custom property with three required declarations: syntax fixes the allowed value type, for example '<color>' or '<length>', inherits explicitly decides whether the value gets passed to child elements, and initial-value defines the value the property takes when it is set nowhere in the cascade. All three are mandatory; if one is missing, the entire @property rule gets discarded as invalid and the property stays unregistered.
The type from syntax is more than documentation: the browser validates every assigned value against that type and discards invalid assignments, instead of silently accepting them as text the way an unregistered property would. A registered --dot-gap with syntax: '<length>' therefore rejects auto or a color value outright, surfacing typos at parse time instead of only as an inexplicably broken layout.
@property --brand-color {
syntax: '<color>';
inherits: true;
initial-value: #4a1d96;
}
@property --card-spacing {
syntax: '<length>';
inherits: false;
initial-value: 16px;
}
3. inherits: false in detail: encapsulating component-local values
Setting inherits: false makes the custom property behave, for inheritance purposes, like an ordinary, non-inheriting CSS property such as margin: a child element only ever sees the initial-value, not the value set further up the cascade on an ancestor, unless the child explicitly inherits it via the inherit keyword or the property gets set again directly on the child.
That is exactly the behavior you want for genuine component-internal values, for example a card component's internal padding, which should not accidentally leak into a deeply nested subcomponent just because it happens to reuse the same property name for a completely different purpose. inherits: false turns custom properties into a real encapsulation tool, something that simply does not exist in CSS without @property.
.card {
--card-spacing: 16px;
padding: var(--card-spacing);
}
.card .nested-widget {
/* --card-spacing here is 16px again (the initial-value),
NOT the 16px set on .card -- because inherits: false */
padding: var(--card-spacing, 8px);
}
4. A common bug: unexpected leaking of unregistered properties
The most frequent bug in large CSS codebases happens when a team introduces a custom property in a deeply nested component for a local purpose, for example --icon-size inside a button, without registering it. Because the property always inherits without @property, a completely unrelated ancestor further up the tree that happens to reuse the same name for something else entirely can suddenly hand an unexpected value down to that button.
Bugs like this are especially hard to debug because DevTools correctly shows the computed value and where the inheritance comes from, but developers rarely think to check there because they assumed the property was purely local. The most reliable prevention is registering every custom property that is not meant to inherit globally with @property and inherits: false from the start, instead of relying on naming conventions like a leading --_.
5. invalid-at-computed-value-time: the fallback difference for invalid values
Without registration, custom properties follow the concept of a "guaranteed-invalid value": when an unregistered property is assigned a syntactically invalid value, it behaves as if unset when consumed through var(), and the fallback value of var(--x, fallback) kicks in. For a property registered with @property, behavior differs in a way that surprises many people: an invalid value does not fall back to the var() fallback, it falls back to the initial-value defined in @property.
That difference becomes a trap when a team habitually adds a var() fallback as a safety net without realizing that fallback no longer applies at all for a registered property in an error case. Anyone using @property needs to treat initial-value as the real, sole fallback mechanism and view the var() fallback only as additional documentation, not as a functional guarantee.
@property --gap {
syntax: '<length>';
inherits: false;
initial-value: 8px;
}
.box {
--gap: not-a-length; /* invalid for the registered <length> syntax */
/* Falls back to the initial-value (8px), NOT to the var() fallback below */
gap: var(--gap, 24px);
}
6. A registration strategy for a consistent design system
In a growing design system it pays off to register every custom property that serves as a design token in a single, central stylesheet file with @property rules, instead of scattering registration across individual component files. This central registry makes it visible at a glance which tokens exist in the system, what type they carry, and whether they are meant to inherit globally or stay component-local.
As a rule of thumb for the registration strategy: color and typography tokens that are deliberately meant to be passed down the whole tree get inherits: true, while component-internal layout values such as internal spacing or icon sizes are consistently registered with inherits: false, so they can never accidentally reach into a foreign component.
7. CSS.registerProperty(): the same registration at runtime through JavaScript
Alongside the declarative @property rule in a stylesheet, there is an equivalent JavaScript API called CSS.registerProperty() that sets the same three declarations (name, syntax, inherits, initialValue) at runtime. That is useful when a design system generates its tokens dynamically from a configuration file or a theme object, so registration cannot exist as static CSS and only becomes known at runtime.
One important difference from the @property rule is that CSS.registerProperty() throws an exception when called with a name that is already registered, while two conflicting @property rules in a stylesheet simply resolve according to the normal cascade rules. Anyone registering tokens both through JavaScript and through a stylesheet should clearly decide which of the two paths is the single source of truth for each property, to avoid runtime errors from duplicate registration.
8. Performance effect: registered custom properties and animation
Registered custom properties bring a practical bonus alongside type safety: because the browser knows from syntax that, say, --dot-gap is a length, it can interpolate discretely between two values, an effect that does not work with unregistered custom properties, because the browser only sees text there, and text cannot be interpolated. A transition on an unregistered custom property therefore jumps abruptly from the old to the new value instead of smoothly blending.
In addition, the style engine can decide earlier for registered properties whether a change even needs to trigger recomputation of downstream values, because type and inheritance behavior are already fixed at registration time instead of being re-derived from context on every use. In very large stylesheets with many custom property references, that can show up measurably in style recalculation time.
9. Browser support and migrating existing custom properties
@property is now supported by Chrome, Edge, Firefox, and Safari, so gradually migrating existing, unregistered custom properties in production projects today carries little risk. What matters is that an @property rule behaves additively: existing var() calls keep working unchanged, only the described fallback behavior for invalid values changes once registration takes effect.
A staged approach is recommended for migration: register the custom properties that get animated most often, or that have caused confusion in deeply nested components, first, and only then move on to the remaining, less critical tokens. That lets the effect of each individual registration be observed in isolation, instead of switching every property at once and losing track of which change caused a regression bug.
| Behavior | Unregistered property | @property with inherits: true | @property with inherits: false |
|---|---|---|---|
| Inherits to children | Always, like a text token | Yes, like a normal inheriting property | No, like margin or padding |
| Invalid value | Falls back to the var() fallback | Falls back to the initial-value | Falls back to the initial-value |
| Animatable | No, only text interpolation | Yes, typed interpolation | Yes, typed interpolation |
| Type checking on assignment | None | Yes, checked against syntax | Yes, checked against syntax |
Mironsoft
Modern CSS, layout architecture and rendering performance
CSS that stays maintainable instead of breaking with every change?
We review existing stylesheets for specificity chaos and layout thrashing, then build a CSS architecture with cascade layers, custom properties and modern layout primitives that still makes sense after the tenth feature.
CSS Audit
Systematically uncovering specificity issues, cascade conflicts and unused selectors.
Architecture Refactoring
Introducing cascade layers, custom properties and design tokens cleanly.
Performance Tuning
Fixing layout thrashing, expensive selectors and rendering bottlenecks.
10. Summary
Custom Properties and @property: The Essentials at a Glance
Default behavior
Unregistered custom properties always inherit, regardless of the name or the intended scope.
Encapsulation
@property with inherits: false turns a custom property into a real, non-inheriting value like margin.
Fallback trap
For registered properties, invalid values fall back to the initial-value, not to the var() fallback.
Strategy
Register design tokens centrally with @property, global tokens with inherits: true, component-local ones with inherits: false.