The Surprising Things That CSS Can Animate

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.

Hey, you reached the end!

Feel free to check out my other blog posts or subscribe to my RSS feed. You can also click a tag below to see related blog posts.