How I Learnt To Stop Worrying And Love Animating The Box Model
As someone who likes to animate things on the web, my general rule of thumb has always been:
- only animate the GPU-accelerated CSS properties
will-changewhen a layer is going to change regularly, keeping it "promoted"
Doing these two things keeps your animations 'cheap', because your browser doesn't have to redraw anything during the animation. But it's incredibly limiting.
This post is just some simple thoughts on animating a humble box as an accordion. This just means an element which open and closes vertically with an animation.
I'm going to cover some nuances on this, but the short version of this post is: for a few seconds, it's probably fine, even if you're animating the
height (or friends) property to do so. 🪗
First, a sweet demo!
⚠️ If you open Chrome's DevTools and enable Paint flashing before toggling the demo, note that this paragraph doesn't flash: we've given it its own layer via
Other browsers may vary.
This demo combines a bunch of concepts that I'll talk about below. Read on 👇
You can build accordions in a bunch of ways, but basically you animate an outer element's
max-height while also setting
There's two ways to do this:
max-heightbetween zero (hidden) and something ridiculous (like
100vh). This is fine, but has odd timing issues: the animation still has to run between your element's actual height and the large value, but there's no visible changes here.
Determine the contained element's actual height with JS (via
.offsetHeightor similar), so you can animate the accordion's
heightto that exact value. However, if the page's width changes, an open accordion can look incorrect as its content isn't measured again.
You can solve the second approach's issue by using
ResizeObserver, and measuring the correct height of the accordion again.
Although this fix is really uncommon out there in the real world—try resizing other pages with open accordions—the demo above does this.
The demos in this article actually use a hack with negative
margin-top to shrink their size.
It actually goes further: for the third red element, it sets its width to the height of the black content element.
We then animate
-100%, as a margin specified in percent always reflects the width of an element—even when applied to the top or bottom margin.
Confused? Well, me too, but this has a bunch of advantages I'd happily tell you all about on Twitter or if you ever get me in the room with a whiteboard. 🤔🖌️
Er, otherwise, read on ⬇️
Any element after an accordion has to redraw constantly during an animation.
The elements are fundamentally being repositioned on the parent element (eventually flowing through to
<body>), so the browser has to do work.
It's possible to create solo layers that get moved around without being redrawn—like the paragraph immediately following the demo—but this is needless extra layer cost for the vast majority of your time the page isn't moving around.
In the ideal world, we'd "chunk" long-running pages into distinct parts: a static section, an accordion section, a static section, and so on. These sections could be wrapped in layers as needed, so that one section expanding or collapsing doesn't effect another. (Many accordion libraries out there in the real world do just this but only for the elements they control, neglecting the page below them.)
Here's a demo where you can play around with these concepts. By toggling the checkboxes on the left, you can force each individual section to have a layer; you can toggle the accordion parts with the larger button.
If you're on a browser which supports the CSS Painting API, the last section actually has a white box which will update every time it is rendered. Try putting it into a layer and toggling the accordion above it—the paint example won't render as it moves.
You can enable Paint flashing in your browser instead, but the paint example is a good way to show what's being repainted anyway. Amusingly, Chrome actually uses multiple threads for painting (which makes redraw fast generally), so the "line" may flash around a bit as different threads draw (they have an independent counter).
You can also hint to a layer that it doesn't need to draw with the new
containCSS property, although it still needs to be composited together with the layer behind it (in our case, the background of
<body>). In practice this means that the content could be cached (e.g., the paint won't redraw) but it still needs to be combined.
The demo is fine and all, but what should you do with this information? Well…
Ideally, a page would actually turn any content following an accordion into a layer just for the duration of an animation—it'll draw once at the start and end, but not at 60fps (or 120fps on say, a modern iPad) during.
This might be infeasible, depending on the way your page is laid out, or it might involve many layers to deal with the tree structure of HTML.
Having said that—how long is your animation? If this is opening and closing in say, .5 seconds, that's 30-60 frames at most, and it's only when a user is actively interacting with your content anyway. Can you let the user's CPU run for that time? 🤔
Chrome uses multiple threads to rasterize content, which means your painting is pretty fast. (We see this via the CSS Painting API, but it's visible in DevTools for everything else too.) But this argument could also be made to keep rendering all the time—yeah, rendering is great on modern machines with umpteen cores. Not so much on your 2015-era low-end Moto G3's…
An alternative for accordions that choose between multiple things—think, a FAQ section or something—is to give the illusion of shifting choice while all being contained in a fixed size box. That outer box could, if you wanted to get tricky, actually be the size of the greatest contained element.
That's all! I just wanted to write about accordions, and I hope it's been enlightening. Please @-me if you want to say hi or let me know how you accordion!