Home / Articles / Color & Theming /
@property: typed custom properties
Register custom properties with a syntax type, initial value, and inheritance — unlocking smooth color animations and type-safe design tokens.
Why custom properties cannot animate by default
Standard CSS custom properties (declared with --name: value) are treated as untyped strings. The browser has no idea whether --accent holds a color, a length, or a random string. Because it cannot interpolate between two arbitrary strings, transitions and animations on custom properties simply snap from one value to another.
The @property at-rule solves this by registering a custom property with an explicit type. Once the browser knows a property holds a <color>, it can smoothly interpolate between two color values.
/* Without @property — the color snaps instantly */
.card {
--bg: oklch(0.19 0.02 260);
background: var(--bg);
transition: --bg 0.3s;
}
.card:hover {
--bg: oklch(0.25 0.04 265);
/* Nothing animates — browser sees two strings */
}
/* With @property — smooth color transition */
@property --bg {
syntax: "<color>";
inherits: false;
initial-value: oklch(0.19 0.02 260);
}
.card {
background: var(--bg);
transition: --bg 0.3s;
}
.card:hover {
--bg: oklch(0.25 0.04 265);
/* Smoothly animates through OKLCH color space */
}
The @property syntax
The @property rule has three required descriptors: syntax, inherits, and initial-value. All three must be present for the registration to be valid.
@property --gradient-angle {
syntax: "<angle>"; /* what type of value this holds */
inherits: false; /* does it inherit down the DOM tree? */
initial-value: 0deg; /* default when no value is set */
}
/* Supported syntax types:
"<color>" — any CSS color value
"<length>" — px, rem, em, etc.
"<percentage>" — 0% to 100%
"<number>" — unitless number
"<angle>" — deg, rad, turn
"<integer>" — whole numbers only
"<length-percentage>" — length or percentage
"<custom-ident>" — a CSS identifier
"*" — any value (no type checking)
*/
Setting inherits: false means child elements do not receive the property from their parent. This is usually what you want for component-scoped values. Set it to true for theme-level tokens that should cascade.
Animating gradient hues
One of the most popular use cases for @property is animating values inside gradients. You cannot transition a background gradient directly, but you can transition a typed custom property that the gradient references.
@property --hue-start {
syntax: "<number>";
inherits: false;
initial-value: 265;
}
@property --hue-end {
syntax: "<number>";
inherits: false;
initial-value: 300;
}
.gradient-card {
background: linear-gradient(
135deg,
oklch(0.5 0.2 var(--hue-start)),
oklch(0.6 0.18 var(--hue-end))
);
transition: --hue-start 0.6s, --hue-end 0.6s;
}
.gradient-card:hover {
--hue-start: 200;
--hue-end: 170;
/* The gradient hues shift smoothly from purple → teal */
}
This technique is also powerful for @keyframes animations. You can create endlessly cycling gradient effects by animating the hue property through 360 degrees.
Animating color stops in keyframes
Combining @property with @keyframes lets you create animated gradients that loop smoothly. Here is a background animation that cycles through the color wheel:
@property --gradient-hue {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
.animated-bg {
background: linear-gradient(
135deg,
oklch(0.45 0.22 var(--gradient-hue)),
oklch(0.55 0.18 calc(var(--gradient-hue) + 60deg))
);
animation: rotate-hue 8s linear infinite;
}
@keyframes rotate-hue {
to {
--gradient-hue: 360deg;
}
}
/* The gradient smoothly cycles through every hue,
maintaining consistent perceived brightness thanks to OKLCH */
Without @property, the --gradient-hue value would snap from 0 to 360 in a single frame. Type registration is what makes smooth interpolation possible.
Type-safe design tokens
Beyond animation, @property provides type safety for your design tokens. If a component expects a color and receives a length, the initial value kicks in instead of breaking the layout.
@property --surface-color {
syntax: "<color>";
inherits: true;
initial-value: oklch(0.19 0.02 260);
}
@property --radius {
syntax: "<length>";
inherits: false;
initial-value: 0.5rem;
}
.card {
background: var(--surface-color);
border-radius: var(--radius);
}
/* If someone accidentally writes: */
.broken {
--surface-color: 42px;
/* Browser ignores invalid value, uses initial-value instead.
The card still gets oklch(0.19 0.02 260) — not a broken layout. */
}
This fallback behavior makes @property especially useful in design systems where custom properties are consumed by many different components. A typo or misconfiguration produces a graceful fallback rather than a visual bug.
Practical patterns
Here are patterns that combine @property with OKLCH for real-world UI components:
/* Pattern 1: Smooth theme switching */
@property --theme-bg {
syntax: "<color>";
inherits: true;
initial-value: oklch(0.13 0.02 260);
}
@property --theme-text {
syntax: "<color>";
inherits: true;
initial-value: oklch(0.93 0.01 260);
}
body {
background: var(--theme-bg);
color: var(--theme-text);
transition: --theme-bg 0.5s ease, --theme-text 0.5s ease;
}
/* Pattern 2: Progress bar with animated color */
@property --progress-hue {
syntax: "<number>";
inherits: false;
initial-value: 25;
}
.progress-bar {
background: oklch(0.6 0.2 var(--progress-hue));
transition: --progress-hue 0.4s, width 0.4s;
}
/* Red (25) at 0%, yellow (85) at 50%, green (145) at 100% */
.progress-bar[data-value="50"] { --progress-hue: 85; }
.progress-bar[data-value="100"] { --progress-hue: 145; }
Browser support for @property is excellent in 2026: Chrome 85+, Safari 15.4+, Firefox 128+. It is safe to use without a fallback in most projects.