When you think of animating CSS properties, which ones come to mind? I recently started wondering about the ones that don’t come to mind — properties that aren’t typically associated with animation, but turn out to be animatable.
This articles explores some of the unexpected things that CSS can animate and some nifty things you can do by animating them.
Understanding Interpolation
When your browser animates a CSS property, it creates a sequence of intermediate values over the course of the animation. This is referred to as interpolation.
Here’s a widget that will animate height
on a <div>
and show the interpolated values during the animation. Give it a try.
See the Pen Interpolation Widget (height) by Will Boyd (@lonekorean) on CodePen.
The results are pretty much what you’d expect. Interpolating height
values from 80px
to 160px
is straightfoward: it’s a series of incremented values.
Now let’s do z-index
.
See the Pen Interpolation Widget (z-index) by Will Boyd (@lonekorean) on CodePen.
Yes, you can animate z-index
! It’s not visually apparent in this demo, but seeing the interpolated values go from 1
to 5
confirms it. What’s interesting is that we don’t see any decimal points like we did for height
. We only get integers, which makes sense for z-index
.
Alright, let’s do something wacky. What happens if we try to animate text-transform
from lowercase
to uppercase
?
See the Pen Interpolation Widget (text-transform) by Will Boyd (@lonekorean) on CodePen.
Something happened! Unless you’re using Safari (sorry). But for everyone else, you can see the text flip from “div” to “DIV”. This is known as a discrete animation and the W3C Web Animations working draft describes the behavior as follows:
The property’s values cannot be meaningfully combined, thus it is not additive and interpolation swaps from Va to Vb at 50% (p=0.5)…
In other words, if there’s not a sensible way to gradually interpolate from the start value to the end value, then just switch between them halfway through.
And there’s our spark. If we look beyond properties with obvious gradual visual transitions, we’ll find that a lot of unexpected properties can be animated, either discretely or with unique interpolations.
Layered Animations
Let’s go back to z-index
. You can animate z-index
to achieve layered animations. Here’s an example with Big Boo floating through walls.
See the Pen Spooky Boo! by Will Boyd (@lonekorean) on CodePen.
There are 5 walls, each with a z-index
of 1
through 5
. The Big Boo has its z-index
animated from 1
to 6
, moving it in front of each wall, before alternating back to move behind each wall.
.boo-move {
animation: move 6s ease-in-out infinite alternate;
}
@keyframes move {
from {
z-index: 1;
/* `transform` here to appear further away */
}
to {
z-index: 6;
/* `transform` here to appear closer */
}
}
There are additional transform
declarations and a separate bob
animation to complete the effect of the Big Boo floating hauntingly towards us as it goes through walls. Check the CSS in the demo if you’re interested.
Here’s another demo that plays with z-index
, this time to animate overlapping cards.
See the Pen Overlapping Sushi Cards by Will Boyd (@lonekorean) on CodePen.
To swing a card forwards, we apply this animation.
@keyframes swing-forwards {
0% {
z-index: 1;
/* `transform` here to appear further away */
}
50% {
/* `transform` here to swing outwards */
}
100% {
z-index: 2;
/* `transform` here to overlap inwards and appear closer */
}
}
The z-index
starts at 1
(behind) and ends at 2
(in front).
At the same time, the other card swings backwards with this animation. It’s essentially the same, but reversed.
@keyframes swing-backwards {
0% {
z-index: 2;
/* `transform` here to overlap inwards and appear closer */
}
50% {
/* `transform` here to swing outwards */
}
100% {
z-index: 1;
/* `transform` here to appear further away */
}
}
In both cases, the z-index
value flips right in the middle, at 50%
. This is the moment when the cards are not overlapping, so changing the z-index
won’t cause a card to clip through the other.
Animating CSS Generated Content
Here’s a fun one. This demo is an auto-incrementing binary counter with decimal output using no JavaScript. Works everywhere but Safari. And no, I’m not cheesing that decimal output by hardcoding 0
through 15
somewhere.
See the Pen CSS Binary Counter by Will Boyd (@lonekorean) on CodePen.
The demo works by (discretely) animating 2 properties: content
and counter-increment
. Here’s the relevant CSS.
.widget-wrapper {
counter-reset: sum;
}
.digit-4 { --increment: 8; }
.digit-3 { --increment: 4; }
.digit-2 { --increment: 2; }
.digit-1 { --increment: 1; }
.digit::before {
content: '0';
animation: flip-digit step-end infinite;
animation-duration: calc(var(--increment) * 2s);
}
@keyframes flip-digit {
50% {
content: '1';
counter-increment: sum var(--increment);
}
}
.decimal-output::before {
content: counter(sum);
}
Each digit has --increment
set with its value in decimal. Conveniently, this value is also used to calculate the digit’s animation-duration
. For example, .digit-4
has an --increment
value of 8
so its animation-duration
works out to 16s
. Its content
property is animated to be '0'
for half that time, then flip to '1'
for the other half, repeating infinitely.
The decimal output uses a CSS counter, a neat feature of CSS that lets you keep a running tally. A sum
counter is started on .widget-wrapper
, then whenever a digit’s content
is animated to '1'
, we also animate counter-increment
to add its --increment
value to sum
. The resulting total of sum
is displayed in .decimal-output
.
What’s neat is that .decimal-output
isn’t actually animated. It’s simply reflecting the result, in real-time, of various counter-increment
properties being animated.
Fade and Go Away
Consider a modal that you want to fade out when dismissed. Animating opacity
takes care of that. But even when an element reaches opacity: 0
it’s still there, invisibly covering page content and blocking clicks.
You could use a bit of JavaScript with setTimeout
to get rid of the modal at the right time. Another option is to (discretely) animate visibility
.
.modal {
animation: dismiss 1s forwards;
}
@keyframes dismiss {
to {
opacity: 0;
visibility: hidden;
}
}
And here it is in action.
See the Pen Fading Modal by Will Boyd (@lonekorean) on CodePen.
But wait a second… if visibility
is animated discretely, shouldn’t it flip to hidden
halfway through the animation, before the fade has finished? Normally yes, but visibility
has special interpolation rules!
For the visibility property, visible is interpolated as a discrete step where values of p between 0 and 1 map to visible and other values of p map to the closer endpoint…
In other words, visibility
won’t flip to hidden
until the very last moment. Here’s that interpolation widget again so you can see for yourself.
See the Pen Interpolation Widget (visibility) by Will Boyd (@lonekorean) on CodePen.
Text Selection Trick
Here’s a widget that lets users click once to select all text, then click again to select some of the text. This is something I wrote about in my previous article, so check there if you want more context. Doesn’t work in Safari, but degrades reasonably well.
See the Pen Select All... Then Select Some by Will Boyd (@lonekorean) on CodePen.
The key piece is a discrete animation that changes user-select
from all
to text
after the element containing the text is focused.
code {
user-select: all;
}
code:focus {
animation: select 100ms step-end forwards;
}
@keyframes select {
to { user-select: text; }
}
Note that the <code>
element needs to be made focusable with tabindex
for this to work.
<code tabindex="0">code snippet goes here</code>
Animating Shadows
The box-shadow
property can take multiple sets of values, as a comma-separated list, to create multiple shadows. Here’s some CSS we’ll be using soon.
div {
box-shadow:
/* in order: offset-x, offset-y, blur-radius, color */
0 0 20px #fff,
-20px 0 80px #f0f,
20px 0 80px #0ff,
/* same, except these shadows go inwards */
inset 0 0 50px #fff,
inset 50px 0 80px #f0f,
inset -50px 0 80px #0ff,
inset 50px 0 300px #f0f,
inset -50px 0 300px #0ff;
}
By animating box-shadow
, you animate a list of shadows into another list of shadows. Each shadow in the starting list is interpolated to the corresponding shadow in the ending list. Each interpolation is smooth, unless one has inset
and the other doesn’t, in which case the entire animation falls back to being discrete.
Let’s add the following CSS to animate the box-shadow
from earlier. This will animate the offset-x
values of some of the shadows.
div {
animation: pulsate 6s linear infinite;
}
@keyframes pulsate {
50% {
box-shadow:
0 0 20px #fff,
20px 0 80px #f0f,
-20px 0 80px #0ff,
inset 0 0 50px #fff,
inset -50px 0 80px #f0f,
inset 50px 0 80px #0ff,
inset -50px 0 300px #f0f,
inset 50px 0 300px #0ff;
}
}
The result is a soft pulsating effect, with colored shadows shifting side to side.
See the Pen Pulsating Marble by Will Boyd (@lonekorean) on CodePen.
The same concept applies to text-shadow
. Hover/tap that chicken nugget to see another animation using multiple shadows.
See the Pen That Chicken Nugget by Will Boyd (@lonekorean) on CodePen.
The HTML wraps each word in its own <span>
.
<div class="words">
<span>look</span>
<span>at</span>
<span>that</span>
<span>chicken</span>
<span>nugget</span>
</div>
And here’s the relevant CSS.
.words span {
text-shadow: 5px 5px 0 #ccc;
}
/* add animation to each word when nugget is hovered */
.nugget:hover ~ .words span {
animation-name: pop-word;
animation-duration: 1s;
animation-fill-mode: forwards;
}
@keyframes pop-word {
to {
text-shadow:
5px 5px 0 yellow,
10px 10px 0 orange,
15px 15px 0 red,
20px 20px 0 brown;
}
}
/* wait half a second between each word */
.words span:nth-child(2) { animation-delay: 0.5s; }
.words span:nth-child(3) { animation-delay: 1s; }
.words span:nth-child(4) { animation-delay: 1.5s; }
.words span:nth-child(5) { animation-delay: 2s; }
Notice how it animates a single gray shadow to multiple colored shadows. In these cases where the starting and ending lists aren’t the same length, the shorter list is padded with “null” shadows (transparent and lengthless) so the lists match up nicely for interpolation.
Closing Remarks
Hopefully I’ve shown you some interesting things that can be achieved by animating CSS properties not often considered for animation. There are many more properties to play with that this article didn’t get to, so keep exploring!
My intention was to show what’s possible, but as always, please use your own judgement before applying any techniques. Not everything needs to be animated and sometimes JavaScript is the right answer.