Creating smooth CSS animations — even with a heavy DOM
This is a follow up to “The grand lifecycle of a small product change” which covers our design process for this feature.
Purple boards can be pretty heavy in the DOM — we’re loading all of your documents, designs, prototypes, and tasks on a single project board. But this also means that we need to be very careful with how we implement any animations. When we implemented the redesigned flow for inserting a card into your project, we used a nifty technique that we haven’t seen written about elsewhere. So we thought we’d share!
Here’s what we were trying to build with this project — the ability to insert a card between two existing cards, rather than just at the end.
Our first iteration of implementing this interaction totally failed. The challenge was to animate the expansion of the New Card Menu.
In between each card, we put a 0px wide container for the New Card Menu. On click of the + button, we transitioned the width property of that container from 0 to 400px.
It resulted in a framerate of 2–10fps during the animation. Why? When you change CSS properties like width, height, margin, padding, etc, the browser has to run what’s called “layout”. Because the width of one element may change the width or positioning of another element, which will impact other elements, the browser has to recalculate the dimension and position of every DOM element on the screen. So it’s doing all sorts of calculations. Every. Single. Frame.
The Better Way®
What we did instead involves some CSS trickery, so let’s take it step by step.
- Click the + button
- Insert the New Card Menu into the DOM
- Apply transform:translateX(-400px) on each of the subsequent cards
- Start animating the translateX property from -400px to 0px on each of the subsequent cards
Alright, let’s see each of those steps visually.
- User clicks on the + button
2. Add the New Card Menu into the DOM.
Adding the menu into the DOM will trigger layout, but only once — and it happens before the animation starts. But once we add the menu into the DOM, the subsequent cards will jump over to the right.
Obviously, we don’t want that. So what next?
3. Apply transform:translateX(-400px)
Along with Step 2, we immediately apply transform:translateX(-400px) on each subsequent card. This makes it so that the subsequent cards look like they’re in the same place as when they started. See below.
If you asked the DOM for the position of the second card after applying the translateX, it would still give you the coordinates as if it was still in the pink box. But visually, it’s rendered over to the left. That’s why transform:translate is so great — it doesn’t trigger massive changes to the DOM, so layout doesn’t run.
Alright! Now we can animate that card to the right.
4. Start animating the translateX property
Time to run the actual animation. We simply transition from translateX(-400px) to translateX(0px) on the subsequent cards. Layout doesn’t run every frame, so they smoothly animate.
The tl;dr of our solution
In a nutshell, we add the menu into the DOM, and at the same time, we give the subsequent cards a negative translateX so they look like they’re in the same place. Then animate the translateX for a nice smooth animation.
This technique is a serious optimization for a single animation. But when you have a great team, you have the luxury to try to push the browser in new ways. The final animation matches up exactly with our original Principle prototype. You can see it over at Purple.pm — just create a free account and jump into one of the sample projects that you can play around with.
Follow us on Twitter at @purpleapp to hear more about the feature work that we do!