← Back to Index

Lesson 2

@property - Registered Custom Properties

In Lesson 1, custom properties were introduced as untyped - the browser treats them as raw token strings until substitution. This has a real consequence: you can't animate them.

The browser doesn't know if --foo holds a colour, a length, or a sentence. Without type information, it can't interpolate between two values - it can only do a hard swap at 50% of a transition.

The problem: transitioning an unregistered property

Hover both boxes - same transition CSS, different results

Unregistered: hard swap (flips at 50%)

Registered as <color>: smooth transition

/* Unregistered  -  browser can't interpolate, hard swap */
.no-register-box {
  --bg-color: #3a86ff;
  background: var(--bg-color);
  transition: --bg-color 0.8s ease-in-out;
}
.no-register-box:hover { --bg-color: #e76f51; }

/* Registered  -  browser knows it's a <color>, smooth transition */
@property --reg-color {
  syntax: "<color>";
  inherits: false;
  initial-value: #3a86ff;
}
.registered-box {
  background: var(--reg-color);
  transition: --reg-color 0.8s ease-in-out;
}
.registered-box:hover { --reg-color: #e76f51; }

Both boxes use the same pattern - set a custom property, transition it on hover. But only the registered one transitions smoothly. The unregistered one snaps.

The solution: @property

The @property at-rule lets you register a custom property with three required pieces of information:

@property --reg-color {
  syntax: "<color>";       /* 1. What TYPE of value this holds */
  inherits: false;          /* 2. Does it cascade to children? */
  initial-value: #3a86ff;   /* 3. Default when not explicitly set */
}

Once registered, the browser knows --reg-color is a colour. It can interpolate between colour values, so transitions and animations work.

Registration gives the browser three things it needs: a type (so it knows how to interpolate), an inheritance rule (so it knows where the value comes from), and a guaranteed initial value (so it's never undefined).

The three descriptors

1. syntax

Defines what values are valid. Uses CSS value definition syntax in a string:

You can combine types with | for alternatives, append # for comma-separated lists, or + for space-separated lists:

@property --flexible {
  syntax: "<length> | <percentage>";  /* accepts either */
  inherits: false;
  initial-value: 0px;
}

@property --color-list {
  syntax: "<color>#";  /* comma-separated list of colours */
  inherits: true;
  initial-value: red, blue;
}

@property --spacing {
  syntax: "<length>+";  /* space-separated list of lengths */
  inherits: false;
  initial-value: 1rem 2rem;
}

2. inherits

Either true or false.

Recall from Lesson 1: unregistered custom properties always inherit. Registration gives you the choice to stop that.

--card-accent registered with inherits: false

Parent: --card-accent set to orange (#e76f51)
Child: uses var(--card-accent) but gets the initial-value (#3a86ff, blue) - not the parent's orange. Because inherits is false.
@property --card-accent {
  syntax: "<color>";
  inherits: false;         /* children don't get parent's value */
  initial-value: #3a86ff;  /* they get this instead */
}

.parent-card { --card-accent: #e76f51; border: 3px solid var(--card-accent); }
.child-card  { border: 3px solid var(--card-accent); }  /* gets blue (initial-value)! */
When to use inherits: false: component-level properties that should reset per-component, not leak from parents. Think of it as creating a property that behaves like padding (doesn't inherit) rather than color (does inherit).

3. initial-value

The guaranteed default. Used when:

Must be a valid value per the syntax. Required unless syntax is "*".

Type safety: invalid values fall back gracefully

@property --safe-size {
  syntax: "<length>";
  inherits: false;
  initial-value: 16px;
}

.box {
  --safe-size: banana;  /* invalid for <length> */
  padding: var(--safe-size);  /* gets 16px (initial-value), not broken */
}

Compare this to unregistered properties: if you set an unregistered --size: banana and use it in padding: var(--size), the padding declaration becomes invalid at computed-value time and you get the inherited value (or the property's own initial value) - which is often 0. Much harder to debug.

Ad

The big unlock: animating custom properties

This is the killer feature. With registration, you can transition or animate things that were previously impossible in pure CSS:

Transitioning a hue value

Hover to transition --hue from 220 to 340 (smooth interpolation through the colour space)

@property --hue {
  syntax: "<number>";
  inherits: false;
  initial-value: 220;
}

.hue-box {
  background: oklch(0.7 0.15 var(--hue));
  transition: --hue 0.8s ease-in-out;
}

.hue-box:hover {
  --hue: 340;
}

Animating a gradient stop position

You can't transition a gradient directly - the browser sees the whole linear-gradient(...) as a single value. But if you pull out the stop position into a registered property, you can animate just that number:

Hover to animate the gradient stop from 30% to 80%

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

.gradient-stop-box {
  background: linear-gradient(to right, blue var(--stop), transparent var(--stop));
  transition: --stop 0.6s ease-in-out;
}

.gradient-stop-box:hover {
  --stop: 80%;
}
The pattern: extract any numeric part of a "non-animatable" value into a registered custom property. Then transition the property instead of the shorthand. Works for gradient positions, colour channels, clip-path values, individual transform components - anything with a number inside.

Browser support

@property is supported in Chrome/Edge 85+, Safari 16.4+, and Firefox 128+. As of mid-2025, it's safe for production with a reasonable fallback (the property just acts unregistered in older browsers - no animation, but no breakage).

Quiz

Question 1

Why can't you transition an unregistered custom property?

The browser blocks transitions on -- properties
The browser doesn't know the type, so can't interpolate
Transitions only work on standard CSS properties
You need to add will-change for custom properties

Question 2

What are the three required descriptors in an @property rule?

name, type, default
syntax, cascade, fallback
syntax, inherits, initial-value
type, scope, initial-value

Question 3

You register --size with syntax "<length>" and initial-value 16px. Someone sets --size: red. What does var(--size) resolve to?

red - the browser doesn't validate
0px - invalid values become zero
16px - falls back to initial-value
nothing - the declaration is dropped

Question 4

You want to animate a gradient's colour stop from 20% to 80%. What syntax do you register the property with?

syntax: "<gradient>";
syntax: "<percentage>";
syntax: "<number>";
syntax: "<length>";

Question 5

A property is registered with inherits: false. A parent sets it to orange. A child reads it with var(). What does the child get?

orange - custom properties always inherit
the initial-value - inherits: false blocks it
transparent - no value means transparent
an error - you must set it on every element