Back in 2012, Internet Explorer 10 came out and, among other things, it finally supported CSS gradients and, in addition to that, the ability to animate them with just CSS! No other browser supported this at the time, but I was hopeful for the future.
Sadly, six years have passed and nothing has changed in this department. Edge supports animating gradients with CSS, just like IE 10 did back then, but no other browser has added support for this. And while animating background-size
or background-position
or the opacity
or rotation of a pseudo element layered on top can take us a long way in terms of achieving cool effects, these workarounds are still limited.
There are effects we cannot reproduce without adding lots of extra elements or lots of extra gradients, such as “the blinds effect” seen below.
In Edge, getting the above effect is achieved with a keyframe animation
:
html {
background: linear-gradient(90deg, #f90 0%, #444 0) 50%/ 5em;
animation: blinds 1s ease-in-out infinite alternate;
}
@keyframes blinds {
to {
background-image: linear-gradient(90deg, #f90 100%, #444 0);
}
}
If that seems WET, we can DRY it up with a touch of Sass:
@function blinds($open: 0) {
@return linear-gradient(90deg, #f90 $open*100%, #444 0);
}
html {
background: blinds() 50%/ 5em;
animation: blinds 1s ease-in-out infinite alternate;
}
@keyframes blinds { to { background-image: blinds(1) } }
While we’ve made the code we write and what we’ll need to edit later a lot more maintainable, we still have repetition in the compiled CSS and we’re limited by the fact that we can only animate between stops with the same unit — while animating from 0%
to 100%
works just fine, trying to use 0
or 0px
instead of 0%
results in no animation happening anymore. Not to mention that Chrome and Firefox just flip from orange to grey with no stop position animation
at all!
Fortunately, these days we have an even better option: CSS variables!
Right out of the box, CSS variables are not animatable, though we can get transition
(but not animation
!) effects if the property we use them for is animatable. For example, when used inside a transform
function, we can transition
the transform
the property.
Let’s consider the example of a box that gets shifted and squished when a checkbox is checked. On this box, we set a transform
that depends on a factor --f
which is initially 1
:
.box {
/* basic styles like dimensions and background */
--f: 1;
transform: translate(calc((1 - var(--f))*100vw)) scalex(var(--f));
}
When the checkbox is :checked
, we change the value of the CSS variable --f
to .5
:
:checked ~ .box { --f: .5 }
Setting a transition
on the .box
makes it go smoothly from one state to the other:
.box {
/* same styles as before */
transition: transform .3s ease-in;
}
Note that this doesn’t really work in the current version of Edge due to this bug.
However, CSS gradients are background images, which are only animatable in Edge and IE 10+. So, while we can make things easier for ourselves and reduce the amount of generated CSS for transitions (as seen in the code below), we’re still not making progress in terms of extending support.
.blinds {
background: linear-gradient(90deg, #f90 var(--pos, 0%), #444 0) 50%/ 5em;
transition: .3s ease-in-out;
:checked ~ & { --pos: 100%; }
}
Enter Houdini, which allows us to register custom properties and then animate them. Currently, this is only supported by Blink browsers behind the Experimental Web Platform features flag, but it’s still extending support a bit from Edge alone.
Going back to our example, we register the --pos
custom property:
CSS.registerProperty({
name: '--pos',
syntax: '<length-percentage>',
initialValue: '0%',
inherits: true
});
Note that means it accepts not only length and percentage values, but also
calc()
combinations of them. By contrast, |
only accepts length and percentage values, but not calc()
combinations of them.
Note that explicitly specifying inherits
is now mandatory, even though it was optional in previous versions of the spec.
However, doing this doesn’t make any difference in Chrome, even with the flag enabled, probably because, in the case of transitions, what’s being transitioned is the property whose value depends on the CSS variable and not the CSS variable itself. And since we generally can’t transition between two background images in Chrome in general, this fails as well.
It does work in Edge, but it worked in Edge even without registering the --pos
variable because Edge allows us to transition between gradients in general.
What does work in Blink browsers with the flag enabled is having an animation
instead of a transition
.
html {
background: linear-gradient(90deg, #f90 var(--pos, 0%), #444 0) 50%/ 5em;
animation: blinds .85s ease-in-out infinite alternate;
}
@keyframes blinds { to { --pos: 100%; } }
However, this is now not working in Edge anymore because, while Edge can animate between gradient backgrounds, it cannot do the same for custom properties.
So we need to take an alternative approach for Edge here. This is where @supports
comes in handy, since all we have to do is check whether a -ms-
prefixed property is supported.
@function grad($pos: 100%) {
@return linear-gradient(90deg, #f90 $pos, #444 0);
}
html {
/* same as before */
@supports (-ms-user-select: none) {
background-image: grad(0%);
animation-name: blinds-alt;
}
}
@keyframes blinds-alt { to { background-image: grad() } }
Stop positions aren’t the only thing we can animate this way. We can do the same thing for the gradient angle. The idea behind it is pretty much the same, except now our animation
isn’t an alternating one anymore and we use an easeInOutBack
kind of timing function.
@function grad($ang: 1turn) {
@return linear-gradient($ang, #f90 50%, #444 0);
}
html {
background: grad(var(--ang, 0deg));
animation: rot 2s cubic-bezier(.68, -.57, .26, 1.65) infinite;
@supports (-ms-user-select: none) {
background-image: grad(0turn);
animation-name: rot-alt;
}
}
@keyframes rot { to { --ang: 1turn; } }
@keyframes rot-alt { to { background-image: grad(); } }
Remember that, just like in the case of stop positions, we can only animate between gradient angles expressed in the same unit in Edge, so calling our Sass function with grad(0deg)
instead of grad(0turn)
doesn’t work.
And, of course, the CSS variable we now use accepts angle values instead of lengths and percentages:
CSS.registerProperty({
name: '--ang',
syntax: '<angle>',
initialValue: '0deg',
inherits: true
});
In a similar fashion, we can also animate radial gradients. And the really cool thing about the CSS variable approach is that it allows us to animate different components of the gradient differently, which is something that’s not possible when animating gradients as a whole the way Edge does (which is why the following demos don’t work as well in Edge).
Let’s say we have the following radial-gradient()
:
$p: 9%;
html {
--x: #{$p};
--y: #{$p};
background: radial-gradient(circle at var(--x) var(--y), #f90, #444 $p);
}
We register the --x
and --y
variables:
CSS.registerProperty({
name: '--x',
syntax: '<length-percentage>',
initialValue: '0%',
inherits: true
});
CSS.registerProperty({
name: '--y',
syntax: '<length-percentage>',
initialValue: '0%',
inherits: true
});
Then we add the animations:
html {
/* same as before */
animation: a 0s ease-in-out -2.3s alternate infinite;
animation-name: x, y;
animation-duration: 4.1s, 2.9s;
}
@keyframes x { to { --x: #{100% - $p} } }
@keyframes y { to { --y: #{100% - $p} } }
The result we get can be seen below:
We can use this technique of animating the different custom properties we use inside the gradient function to make the blinds in our initial example close the other way instead of going back. In order to do this, we introduce two more CSS variables, --c0
and --c1
:
$c: #f90 #444;
html {
--c0: #{nth($c, 1)};
--c1: #{nth($c, 2)};
background: linear-gradient(90deg, var(--c0) var(--pos, 0%), var(--c1) 0) 50%/ 5em;
}
We register all these custom properties:
CSS.registerProperty({
name: '--pos',
syntax: '<length-percentage>',
initialValue: '0%',
inherits: true
});
CSS.registerProperty({
name: '--c0',
syntax: '<color>',
initialValue: 'red',
inherits: true
});
/* same for --c1 */
We use the same animation as before for the position of the first stop --pos
and, in addition to this, we introduce two steps()
animations for the other two variables, switching their values every time an iteration of the first animation
(the one changing the value of --pos
) is completed:
$t: 1s;
html {
/* same as before */
animation: a 0s infinite;
animation-name: c0, pos, c1;
animation-duration: 2*$t, $t;
animation-timing-function: steps(1), ease-in-out;
}
@keyframes pos { to { --pos: 100%; } }
@keyframes c0 { 50% { --c0: #{nth($c, 2)} } }
@keyframes c1 { 50% { --c1: #{nth($c, 1)} } }
And we get the following result:
We can also apply this to a radial-gradient()
(nothing but the background
declaration changes):
background: radial-gradient(circle, var(--c0) var(--pos, 0%), var(--c1) 0);
The exact same tactic works for conic-gradient()
as well:
background: conic-gradient(var(--c0) var(--pos, 0%), var(--c1) 0);
Repeating gradients are also an option creating a ripple-like effect in the radial case:
$p: 2em;
html {
/* same as before */
background: repeating-radial-gradient(circle,
var(--c0) 0 var(--pos, 0px), var(--c1) 0 $p);
}
@keyframes pos { 90%, 100% { --pos: #{$p} } }
And a helix/rays effect in the conic case:
$p: 5%;
html {
/* same as before */
background: repeating-conic-gradient(
var(--c0) 0 var(--pos, 0%), var(--c1) 0 $p);
}
@keyframes pos { 90%, 100% { --pos: #{$p} } }
We can also add another CSS variable to make things more interesting:
$n: 20;
html {
/* same as before */
background: radial-gradient(circle at var(--o, 50% 50%),
var(--c0) var(--pos, 0%), var(--c1) 0);
animation: a 0s infinite;
animation-name: c0, o, pos, c1;
animation-duration: 2*$t, $n*$t, $t;
animation-timing-function: steps(1), steps(1), ease-in-out;
}
@keyframes o {
@for $i from 0 to $n {
#{$i*100%/$n} { --o: #{random(100)*1%} #{random(100)*1%} }
}
}
We need to register this variable for the whole thing to work:
CSS.registerProperty({
name: '--o',
syntax: '<length-percentage>',
initialValue: '50%',
inherits: true
});
And that’s it! The result can be seen below:
I’d say the future of changing gradients with keyframe animations looks pretty cool. But in the meanwhile, for cross-browser solutions, the JavaScript way remains the only valid one.
Awesome!!!!
For easier/more “declarative” JS-based gradient animations, I’ve written
animate-backgrounds
, which lets you hook into standard jQuery or anime.js animations and animate gradients like any other CSS propertyHere’s a browser-based tool for playing around with customizing (via Sass mixins) and animating gradient patterns like the ones found in Lea Verou’s pattern gallery. Think “animated plaid”