Typed variables: @property
A regular CSS variable from the lesson "CSS variables" is just a piece of text. The browser does not know that --accent holds a color and --angle an angle, so it cannot change their values smoothly. The @property rule registers a variable: it states its type, whether it is inherited and what it equals by default.
A variable without a type
A gradient cannot be animated directly, but you can change a color inside it through a variable. Hover over the button:
<style>
.btn {
--from: tomato;
padding: 12px 24px;
border: 0;
color: white;
background: linear-gradient(90deg, var(--from), gold);
transition: --from 0.6s;
}
.btn:hover { --from: rebeccapurple; }
</style>
<button class="btn">Hover me</button>The color changed with a jump, even though a transition is set: to the browser tomato and rebeccapurple are just two words with no in-between values.
The @property rule
Let us register --from as a color — and the same transition becomes smooth:
<style>
@property --from {
syntax: "<color>";
inherits: false;
initial-value: tomato;
}
.btn {
padding: 12px 24px;
border: 0;
color: white;
background: linear-gradient(90deg, var(--from), gold);
transition: --from 0.6s;
}
.btn:hover { --from: rebeccapurple; }
</style>
<button class="btn">Hover me</button>Now the browser knows it is a color and computes all the shades between the two values.
Three descriptors
| Descriptor | What it sets |
|---|---|
syntax | the value type in quotes |
inherits | true — children get the value from the parent, false — they do not |
initial-value | the value if the variable is not set anywhere |
Types are written in angle brackets:
| Type | Example value |
|---|---|
<color> | tomato, #ff6347 |
<length> | 12px, 2rem |
<percentage> | 50% |
<number> | 1.5 |
<angle> | 45deg, 0.5turn |
* | anything, like a regular variable |
A rotating angle
An angle can be animated too. A conic gradient conic-gradient() paints a block in sectors around the center, like a clock face, and the word from sets the angle it starts at. Let us put the --angle variable there:
<style>
@property --angle {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
@keyframes spin {
to { --angle: 360deg; }
}
.ring {
width: 120px;
height: 120px;
border-radius: 50%;
background: conic-gradient(from var(--angle), tomato, gold, seagreen, tomato);
animation: spin 3s linear infinite;
}
</style>
<div class="ring"></div>It is not the block that rotates, only the gradient inside it. Shimmering button borders are made the same way.
Type checking
A registered variable does not accept a value of another type: the browser skips such a declaration and takes initial-value:
<style>
@property --gap {
syntax: "<length>";
inherits: false;
initial-value: 8px;
}
.box { margin: 4px 0; padding: var(--gap); background: lavender; }
.big { --gap: 30px; }
.wrong { --gap: red; }
</style>
<div class="box big">--gap: 30px</div>
<div class="box wrong">--gap: red — not a length, 8px is used</div>@property is supported in all major browsers since 2024.
Common mistake
Leaving out initial-value or inherits. Without them the whole @property rule is invalid, the variable stays a regular one — and the animation jumps again:
<style>
@property --from {
syntax: "<color>";
inherits: false;
}
.btn {
--from: tomato;
padding: 12px 24px;
border: 0;
color: white;
background: linear-gradient(90deg, var(--from), gold);
transition: --from 0.6s;
}
.btn:hover { --from: rebeccapurple; }
</style>
<button class="btn">A jump: no initial-value</button>Only the * type may go without an initial value. For every other type write all three descriptors.