Register CSS custom properties with @property for animation and type safety
rule · css-at-property
CSS custom properties (variables) are string substitutions by default. The browser does not know whether --brand-color is a color, a length, or a number, which is why @property (opens in a new tab) and the Properties & Values API spec (opens in a new tab) exist: they let the engine treat a custom property as a real typed value.
Code Example
@property --property-name {
syntax: '<color>'; /* The type — see valid syntax descriptors below */
initial-value: #000; /* Required — the property's default value */
inherits: true; /* Whether the value cascades to child elements */
}Why It Matters
CSS custom properties are powerful but limited by default: they are untyped string substitutions that the browser cannot interpolate during transitions or animations. The web.dev @property article (opens in a new tab) shows why registration matters in practice: the browser finally has enough information to animate between values, validate token assignments, and scope inheritance cleanly.
Syntax Descriptors
/* Primitive types */
syntax: '<color>'; /* #hex, rgb(), hsl(), oklch(), color names */
syntax: '<length>'; /* px, em, rem, vw, etc. */
syntax: '<number>'; /* unitless numbers */
syntax: '<integer>'; /* integers only */
syntax: '<percentage>'; /* 0% to 100% */
syntax: '<angle>'; /* deg, rad, turn */
syntax: '<time>'; /* s, ms */
syntax: '<length-percentage>'; /* length or percentage */
/* Compound types */
syntax: '<color>+'; /* one or more space-separated colors */
syntax: '<length>#'; /* one or more comma-separated lengths */
/* Universal fallback — behaves like an unregistered property */
syntax: '*';Animating a Gradient via Custom Property
Without @property, gradient stops cannot be animated because the browser treats the whole gradient as an opaque string:
/* ❌ Without @property — animation jumps between discrete values */
.card {
--gradient-angle: 135deg;
background: linear-gradient(var(--gradient-angle), #667eea, #764ba2);
transition: --gradient-angle 0.6s ease; /* has no effect — string transition */
}
.card:hover {
--gradient-angle: 225deg;
}/* ✅ With @property — the angle interpolates smoothly */
@property --gradient-angle {
syntax: '<angle>';
initial-value: 135deg;
inherits: false;
}
.card {
background: linear-gradient(var(--gradient-angle), #667eea, #764ba2);
transition: --gradient-angle 0.6s ease; /* smooth animation */
}
.card:hover {
--gradient-angle: 225deg;
}Animating a Color Token
@property --accent-hue {
syntax: '<number>';
initial-value: 220;
inherits: true; /* children pick up the animated value */
}
:root {
--accent-hue: 220;
--accent: oklch(60% 0.2 var(--accent-hue));
transition: --accent-hue 0.4s ease;
}
/* Shift the hue on a specific section */
.section--warm:hover {
--accent-hue: 30; /* orange */
}
/* The component using --accent sees the interpolated hue mid-transition */
.button {
background: var(--accent);
}Typed Design Tokens
Registering design tokens with @property catches mistakes: assigning a length to a color property becomes a validation error in DevTools, not a silent no-op:
/* Register color tokens with explicit types */
@property --color-brand-primary {
syntax: '<color>';
initial-value: oklch(55% 0.25 265);
inherits: true;
}
@property --color-surface {
syntax: '<color>';
initial-value: #ffffff;
inherits: true;
}
/* Register spacing tokens */
@property --spacing-base {
syntax: '<length>';
initial-value: 1rem;
inherits: true;
}
/* Override in dark mode — type is enforced */
@media (prefers-color-scheme: dark) {
:root {
--color-brand-primary: oklch(70% 0.25 265);
--color-surface: #0f172a;
}
}Scoped Component Tokens with inherits: false
/* A progress bar with an animatable fill */
@property --progress-fill {
syntax: '<percentage>';
initial-value: 0%;
inherits: false; /* Each .progress-bar has its own value — they don't share */
}
.progress-bar {
--progress-fill: 0%;
background: linear-gradient(
to right,
var(--color-brand-primary) var(--progress-fill),
var(--color-surface) var(--progress-fill)
);
transition: --progress-fill 0.5s ease;
}
/* JS sets the value per instance */
/* element.style.setProperty('--progress-fill', '72%') */CSS.registerProperty() — the JavaScript Equivalent
The same registration is available in JavaScript, useful for programmatic token generation:
// Equivalent to the @property at-rule
CSS.registerProperty({
name: '--gradient-angle',
syntax: '<angle>',
initialValue: '135deg',
inherits: false,
})Prefer the CSS @property at-rule for static tokens; use CSS.registerProperty() only for dynamically computed token registrations.
Browser Support
@property is supported in all major browsers as of 2024:
- Chrome/Edge 85+
- Firefox 128+
- Safari 16.4+
Verification
Use the MDN @property reference (opens in a new tab) and the Properties & Values API spec (opens in a new tab) as the baseline for what counts as a valid registration.
- Open DevTools and inspect an element with a registered custom property. The Computed Styles panel should show the resolved typed value (e.g.,
oklch(55% 0.25 265)) rather than the raw variable string. - Apply a CSS transition to the registered property and confirm the animation interpolates smoothly between values rather than jumping discretely.
- Intentionally assign a value of the wrong type to a registered property (e.g., assign a
pxlength to a<color>property) and confirm the browser ignores the invalid value and falls back toinitial-value— demonstrating type enforcement. - Test in Firefox (the last major browser to ship
@property, in version 128) to confirm the registration works and the animations behave identically.