search ]

CSS @property: A Complete Guide with Live Animated Examples

The @property at-rule gives CSS custom properties something they never had: a type system. You can tell the browser that a variable is a color, an angle, or a length, and suddenly things like gradient transitions and type-safe fallbacks just work.

I started using @property after spending too long faking gradient animations with pseudo-elements and opacity tricks. Once I registered the color stops as typed properties, the browser handled the interpolation natively. No JavaScript, no hacks.

What Is @property?

Regular CSS custom properties (variables) are treated as strings by the browser. It stores --brand-color: #3490dc as a raw text value. This means the browser can’t interpolate between two values during a transition because it doesn’t know what type of data it’s holding.

@property solves this by letting you register a custom property with three pieces of information:

  • syntax – the data type (<color>, <angle>, <length>, <number>, etc.)
  • initial-value – a default fallback
  • inherits – whether child elements inherit the value
@property --brand-color {
  syntax: "<color>";
  inherits: true;
  initial-value: #3490dc;
}

All three descriptors are required (except initial-value when syntax is "*").

The Syntax Descriptor

The syntax descriptor defines what values the property accepts. Here are the supported types:

SyntaxAcceptsExample
<color>Any valid color#ff0, rgb(0 0 0), hsl(120 50% 50%)
<length>Lengths with units16px, 2rem, 50vw
<number>Unitless numbers0, 1.5, 42
<percentage>Percentage values50%, 100%
<angle>Angle values45deg, 0.5turn, 3.14rad
<integer>Whole numbers only0, 3, -1
<time>Duration values300ms, 1.5s
<image>Image references/gradientsurl(...), linear-gradient(...)
<transform-function>Transform functionsrotate(45deg), scale(1.2)
"*"Any value (universal)Any valid CSS value

You can also combine types with | for alternatives: "<color> | <length>". Or use + and # for space-separated and comma-separated lists.

Why It Matters: Animating the Impossible

This is the feature that makes @property worth learning. Without it, the browser can’t transition gradients because it sees them as opaque strings. With typed properties, it can interpolate each color stop individually.

Hover over both cards below to see the difference:

Without @property hover me
With @property hover me

The left card uses a regular transition: background. On hover, the gradient snaps instantly because the browser can’t interpolate between two gradient strings. The right card registers each color stop with @property as a <color>, so the browser transitions them smoothly.

Here’s the code for the smooth version:

@property --grad-start {
  syntax: "<color>";
  inherits: false;
  initial-value: #3490dc;
}
@property --grad-end {
  syntax: "<color>";
  inherits: false;
  initial-value: #6574cd;
}

.card {
  background: linear-gradient(135deg, var(--grad-start), var(--grad-end));
  transition: --grad-start 0.8s ease, --grad-end 0.8s ease;
}
.card:hover {
  --grad-start: #e3342f;
  --grad-end: #ffb636;
}

The key insight: you transition the custom property itself, not the background. The browser knows the property is a <color>, so it interpolates between the two color values. The gradient rebuilds on every frame with the interpolated colors.

Animating Angles: Rotating Gradients

The same principle works for angles. Register a property as <angle> and you can animate it in a @keyframes rule. This example creates a continuously rotating conic gradient:

Conic gradient
rotating via
@property
@property --rotate-angle {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

.spinner {
  background: conic-gradient(
    from var(--rotate-angle),
    #AE0E38, #E57622, #ffb636,
    #3490dc, #6574cd, #AE0E38
  );
  animation: spin 4s linear infinite;
}

@keyframes spin {
  to { --rotate-angle: 360deg; }
}

Before @property, rotating a conic gradient required JavaScript or rotating the entire element with transform: rotate(). With a registered <angle> property, you animate just the gradient’s starting angle.

Animated Hue Cycling

By registering a custom property as <number>, you can animate it and feed it into hsl(). This creates a color wave effect where each cell cycles through hues at a different offset:

1
2
3
4
5
6
7
8
@property --hue {
  syntax: "<number>";
  inherits: false;
  initial-value: 220;
}

.cell {
  background: hsl(var(--hue), 65%, 55%);
  animation: hue-cycle 6s ease-in-out infinite alternate;
}
.cell:nth-child(2) { animation-delay: -0.5s; }
.cell:nth-child(3) { animation-delay: -1s; }
/* ... staggered delays for each cell */

@keyframes hue-cycle {
  0%   { --hue: 220; }
  50%  { --hue: 340; }
  100% { --hue: 40; }
}

Type Validation and Safe Fallbacks

With regular custom properties, assigning an invalid value means the property resolves to nothing, and the declaration using it is thrown out entirely. The browser falls through to the inherited or initial value of the native property – which is often not what you want.

@property changes this. If you assign an invalid value to a registered property, the browser falls back to the initial-value you defined instead:

Valid: 24px
--safe-size: 24px
Fallback: 16px
--safe-size: banana
(falls back to initial-value)
@property --safe-size {
  syntax: "<length>";
  inherits: false;
  initial-value: 16px;
}

.element {
  --safe-size: banana;       /* invalid for <length> */
  font-size: var(--safe-size); /* uses 16px, not broken */
}

Controlling Inheritance

Regular custom properties always inherit from parent to child. With @property, setting inherits: false stops the value from leaking down the DOM tree. This is useful for scoped design tokens that shouldn’t bleed into nested components.

inherits: true (default)
Parent: --theme-accent: #ffb636
Child inherits the gold color
inherits: false
Parent: --scoped-accent: #75bbe8
Child gets initial-value (#aeafb4)
@property --scoped-accent {
  syntax: "<color>";
  inherits: false;
  initial-value: #aeafb4;
}

.parent {
  --scoped-accent: #75bbe8;  /* only applies to .parent */
  border-color: var(--scoped-accent); /* blue */
}

.child {
  border-color: var(--scoped-accent); /* gray - initial-value */
}

Animated Borders

Combining @property with conic-gradient gives you animated gradient borders – no pseudo-elements needed. The trick: use a thin outer wrapper with the animated gradient background and an inner element with a solid background, leaving only a 2px gap for the border effect.

Animated Border

The gradient rotates around the card using a single @property angle variable.

@property --border-angle {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

.card-wrapper {
  padding: 2px;
  border-radius: 10px;
  background: conic-gradient(
    from var(--border-angle),
    #AE0E38, #E57622, #ffb636, #3490dc, #AE0E38
  );
  animation: border-spin 3s linear infinite;
}

.card-inner {
  background: var(--light-bg);
  border-radius: 8px;
  padding: 24px;
}

@keyframes border-spin {
  to { --border-angle: 360deg; }
}

This technique works well for gradient borders that need motion.

Animated Progress Bar

Register a <percentage> property and you can animate width smoothly with @keyframes:

@property --progress
@property --progress {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 0%;
}

.progress-fill {
  width: var(--progress);
  animation: fill 3s ease-in-out infinite alternate;
}

@keyframes fill {
  0%   { --progress: 5%; }
  100% { --progress: 92%; }
}

@property vs CSS.registerProperty()

You can also register properties from JavaScript using CSS.registerProperty(). The functionality is identical, but there are trade-offs:

Feature@property (CSS)CSS.registerProperty() (JS)
Declared inCSS fileJavaScript
Render-blockingNo (parsed with CSS)Yes (runs in JS)
Can re-registerLast definition winsThrows if already registered
SSR friendlyYesNo (needs DOM)
CSS.registerProperty({
  name: '--brand-color',
  syntax: '<color>',
  inherits: true,
  initialValue: '#3490dc'
});

Prefer the CSS @property at-rule over CSS.registerProperty() unless you need to register properties dynamically at runtime. The CSS approach is parsed alongside your stylesheets and doesn’t require JavaScript to execute first.

Browser Support

@property reached universal browser support in July 2024:

  • Chrome 85+ (July 2020)
  • Edge 85+ (July 2020)
  • Safari 16.4+ (March 2023)
  • Firefox 128+ (July 2024)

Firefox was the last holdout. With version 128, the feature is now available everywhere. You can use @supports to provide a fallback for older browsers:

@supports (background: paint(id)) {
  /* @property is supported */
}

/* Or feature-test a registered property */
@supports (--test: initial) {
  /* basic custom properties supported */
}

FAQs

Common questions about @property:

What is the difference between @property and regular CSS variables?
Regular CSS variables (--name: value) are untyped strings. The browser stores and passes them as-is. @property registers a variable with a specific type (like <color> or <angle>), an initial fallback value, and an inheritance flag. This gives the browser enough information to animate the property, validate assigned values, and fall back gracefully when something invalid is assigned.
Can I animate gradients with @property?
Yes. Register each color stop as a separate @property with syntax: "<color>", use them in the gradient via var(), then transition or animate the custom properties directly. The browser interpolates between the color values, producing a smooth gradient transition. Without @property, gradients snap instantly because the browser sees them as opaque strings.
Are all three @property descriptors required?
syntax and inherits are always required. initial-value is required unless the syntax is "*" (the universal syntax). If you omit a required descriptor, the entire @property rule is invalid and the browser ignores it.
What happens when an invalid value is assigned to a registered property?
The browser falls back to the initial-value defined in the @property rule. This is different from regular custom properties, where an invalid value causes the declaration to be discarded entirely and the property resolves to its inherited or initial value. Registered properties give you a reliable safety net.
Should I use @property or CSS.registerProperty()?
Prefer the CSS @property at-rule in most cases. It's parsed with the stylesheet, doesn't require JavaScript, and doesn't block rendering. Use CSS.registerProperty() only when you need to register properties dynamically at runtime, for example based on user preferences or A/B test configuration.
Does @property work in all browsers?
Yes, since July 2024. Chrome and Edge have supported it since version 85 (2020), Safari since 16.4 (2023), and Firefox since 128 (July 2024). It's now a Baseline feature available in all modern browsers. For older browsers that don't support it, the property simply behaves as a regular untyped custom property.

Summary

@property fills a real gap in CSS. Before it shipped, custom properties were just string containers. Now they carry type information that unlocks gradient animations, safe fallbacks, and scoped inheritance – all in pure CSS.

If you’re already using CSS variables, @property is the next step. Start with the gradient transition trick since that’s the most visible win, then explore typed fallbacks for your design tokens.

Join the Discussion
0 Comments  ]

Leave a Comment

To add code, use the buttons below. For instance, click the PHP button to insert PHP code within the shortcode. If you notice any typos, please let us know!

Savvy WordPress Development official logo