We can make variables in CSS pretty easily:
:root {
--scale: 1;
}
And we can declare them on any element:
.thing {
transform: scale(var(--scale));
}
Even better for an example like this is applying the variable on a user interaction, say :hover
:
:root {
--scale: 1;
}
.thing {
height: 100px;
transform: scale(var(--scale));
width: 100px;
}
.thing:hover {
--scale: 3;
}
But if we wanted to use that variable in an animation… nada.
:root {
--scale: 1;
}
@keyframes scale {
from { --scale: 0; }
to { --scale: 3; }
}
/* Nope! */
.thing {
animation: scale .25s ease-in;
height: 100px;
width: 100px;
}
That’s because the variable is recognized as a string and what we need is a number that can be interpolated between two numeric values. That’s where we can call on @property
to not only register the variable as a custom property, but define its syntax as a number:
@property --scale {
syntax: "<number>";
initial-value: 1;
inherits: true;
}
Now we get the animation!
You’re going to want to check browser support since @property
has only landed in Chrome (starting in version 85) as of this writing. And if you’re hoping to sniff it out with @supports
, we’re currently out of luck because it doesn’t accept at-rules as values… yet. That will change once at-rule()
becomes a real thing.
Using
@supports not (inherits: true)
will do nothing because inherits is not a valid CSS property in any browsers so the notice will show even if the browser support@property
inherits is a descriptor that is only valid inside the
@property
.To test the support we need to use
at-rule()
but its a new feature not yet implemented (https://www.bram.us/2022/01/20/detect-at-rule-support-with-the-at-rule-function/)Huh! I thought I had tested that but clearly I was wrong!
Fascinating!
What’s the benefit of using this instead of just using numbers? If someone could explain I’d really appreciate it!
I think that’s the point: custom properties can be updated and interpolated, but we have to first define them as a number; otherwise they’re strings instead of being treated as integers.
--my-var: 7;
Doing JS
getComputedStyle().getProperty('--myvar')
returns" 7"
– a string with a leading whitespace. You can’t do calculations with strings.You can animate components of a property’s value independently, with different animation durations, delays, timing functions and so on.
For example, consider a simple
transform
chain with a rotation and then a translation – this is the basic technique for positioning items on a circle.Uniformly rotating an item on a circle of radius
5em
is achieved with:But let’s say we also want to vary the radius of this circle independently of the rotation. Well… before being able to animate custom properties, we couldn’t do it without an extra child or pseudo. That’s because CSS animates entire property values with CSS animations, not parts of them, so we’d have to break the
transform
value to have the rotation on our item and the translation on a child or pseudo.Enter custom properties! They’re properties, so their values can be animated if we register them.
I detailed this with a lot more examples in this article.
In a similar manner, we can animate the angle and the stop position of a
linear-gradient()
differently:Note that gradients cannot be animated at all in any other way in any of the current browsers (though IE10+ and pre-Chromium Edge could animate them).
Or components of a
clip-path
value:Or components of a
filter
value:And regarding the JS part: they make it a lot easier to modify just parts of a value via JS. For example, let’s say we have a cursor-following spotlight created with a
radial-gradient()
:When the cursor moves, we update just the
--x
and--y
values via JS:… instead of writing an entire sausage like:
Honestly, it just seems a bit ridiculous that CSS has not been officially converted to a scripting language by now.
I mean, it’s selector syntax and engine has supplanted DOM methods! So it already handled arrays (of CSS class based nodes).
It clearly has conditional tests with @ rules that even have faux ‘else if’ and ‘else’ capability by wrapping sets of rules into queries for different device parameters and then allowing, I guess, an ‘also’ rather than an ‘else’ with every other rule running as well.
Variables … that are now getting strict type definition support?
That’s more than JS itself has, at least by default, isn’t it? Hence TypeScript’s existence.
Meanwhile we’ve got SASS when might it not be better to boost CSS up to SASS-like capabilities so that it remains under the W3C umbrella?
Hmmmm.
Yes, I know what you’re all thinking: clear separation of style, markup and behaviour sounds sensible. But does it even really exist at present? Maybe much less than we’d like to think.
We can test and adapt code for
@property
support without@supports
.This is what I do to style things differently:
First, register a
--houdini
custom property and set its initial value to1
.Then set a support flag as a custom property
--s
to the--houdini
variable with a fallback of0
. If registering custom properties isn’t supported,--s
is going to have the--houdini
fallback value (0
). If we have support for registering custom properties,--s
will take the initial value we have set for Houdini (1
). So--s
is basically a flag that’s0
for the no support case and1
otherwise. This flag can be used to set different numeric values depending on which case we’re in.Live test.
We can also make some elements be displayed only in the
@property
support case:We can also use the negation of
--s
in other cases:I’ve detailed such techniques in the DRY Switching with CSS Variables articles.