Typed CSS Variables: @property Registration & Computation Phases
- Last updated
- Prerequisites:
- CSS custom properties (var()) basics
- Understanding of CSS inheritance and animation
- css
- custom-properties
- property
- css-variables
- inheritance
- interpolation
Read at your depth
The practical view
Regular custom properties are untyped: --x: 10px; can hold any token sequence, and var() substitutes it as raw text. @property registers a typed custom property with syntax, inherits, and initial-value: @property --angle { syntax: '<angle>'; inherits: false; initial-value: 0deg; }. Typed properties can be interpolated in transitions/animations (the browser knows how to blend 0deg -> 360deg), and they validate values (invalid values are rejected instead of silently passing through). Register with CSS @property blocks or CSS.registerProperty() from JS.
The same idea in other frameworks
React inline styles can set custom properties; @property registration via global CSS enables animating those variables.Angular component styles can include @property blocks for typed theming tokens that animate.Vue SFC <style> blocks can register @property for transition-ready design tokens.Svelte components with global CSS can register typed variables for animated theme values.Legacy vs modern
Unregistered var() animation vs typed @property animation
Animating an unregistered custom property falls back to discrete 'jump' behavior; registering a syntax makes the browser interpolate smoothly.
.card { --hue: 0; transition: --hue 1s; }
.card:hover { --hue: 200; } /* ❌ OLD: untyped → discrete jump, no smooth transition */@property --hue { syntax: '<number>'; inherits: false; initial-value: 0; }
.card { --hue: 0; transition: --hue 1s; } /* ✅ NEW: smoothly interpolates */Interview gotchas
Context
This gets at the core difference between token substitution and typed computed values — interviewers want the animation machinery reason, not just 'register it and it works'.
The mechanical answer
A regular custom property stores its value as an untyped token sequence; when a transition or animation targets it, the engine has no way to compute intermediate values — blending '10px 20px' with '20px 10px' or 'red' with '200' is undefined without a type. So it falls back to discrete (step) interpolation. Registering the property with @property declares a syntax (e.g. <number>, <color>, <angle>), so the engine parses the value into a typed computed value at style-computation time and can interpolate in that type's native space — numbers blend arithmetically, colors interpolate in the color space, angles spin. That type information is what the transition/animation machinery needs to produce smooth intermediate frames.
Trap
The naive answer is 'custom properties just can't be animated in CSS'. They can, once typed — the interpolation needs the syntax declaration. Another trap: assuming the initial-value is only a fallback for missing values — it is also what invalid values resolve to at computed-value time, which changes how inheritance behaves. Candidates who mention computed-value parsing and interpolation in native space show real understanding.
Context
This probes the inheritance semantics and the 'custom property leak' problem — a practical bug that @property registration fixes.
The mechanical answer
By default, custom properties inherit, which is often exactly what you want (theme tokens). But it can leak values unintentionally: an element that sets --hue: 200 makes ALL descendants inherit 200 unless they override it, and because the value is untyped, descendants can also receive malformed tokens that 'work' until they reach a calc() or animation that chokes on them. @property with inherits: false makes the property NOT inherit — each element starts from its initial-value and only uses values set on itself, which is the correct semantics for state-like properties (e.g. --progress). The trade-off is deliberate: inherited tokens should keep inherits: true, while per-element state should be false.
Trap
The naive answer is 'inherits: false just makes the property private'. It changes computed-value semantics — children no longer read the parent's value at all. Another trap: forgetting that untyped variables only substitute text, so a 'leaked' value can produce subtle breakage in calc() or var() chains far from where it was set. Candidates who connect inherits to the computed-value pipeline and give concrete leak scenarios show real depth.