Advanced web animation can be tricky - you want to mount and unmount elements, reduce layout, and keep the whole thing looking smooth. While working on Microsoft Loop, I recently added an animation that plays when you open or close the sidebar. I found that using a finite state machine and the Web Animations API made the animation performant and easy to read, with no need for a library or package. Here’s some examples of how it works!
Breaking down the animation
This simple sidebar and content layout requires a few different moving parts:
- The sidebar must take up 160 pixels when its open, but 0 pixels when its closed.
- We can’t animate the sidebar width since animating width and layout is slow.
- We want the main content to cover the sidebar, rather than just slide the sidebar away.
- When closed, we should remove the
<Sidebar />
React component - but only once the animation is completed.
Why a finite state machine?
A finite state machine abstractly represents all the possible states of a system, along with the possible paths between states. They often have a corresponding state diagram that shows all this information at once.
The sidebar animation has two clear states: the sidebar is open and the sidebar is closed. We also want to include a state for when the sidebar is animating. The sidebar animation is different when its opening vs when its closing, so we need two different states to represent this.
The diagram makes it pretty clear how many states we need to represent the animation and the possible paths. All the animation logic will be driven by the current state of the state machine. Setting up the animation in this way makes it easier to keep React rendering in sync, since the state can be accessed synchronously.
Representing the animation in CSS
Let’s start with the CSS used to represent the sidebar and content layout.
Intuitively you probably want to animate the sidebar column’s width from 0px to 160px. However, this will hurt your website’s performance. Each “in-between” width needs to be calculated by the browser during the animation: 1px, 2px, … 319px, 160px. When the browser is running the animation, it has to re-layout the page for each of those in-between values. As a result, your site will attempt to figure out how every element is laid out around 160 times in a mere third of a second.
The more performant way to build animations is to use the transform
property. Transformations are purely visual and don’t effect layout. As a result, it can be animated cheaply. When animating, we can translate the content section to cover and uncover the sidebar.
Changing layout
However, we still want to change layout eventually, since the content needs to fill the screen once the sidebar closes. So, once the animation is done, we change the width. The difference is this layout change happens once rather than many times.
We also need to change the layout once when the animation is running. In the demo above, you’ll notice that the area behind the content is visible when its being translated. Since the content only takes up part of the screen, it leaves a gap once it starts moving. The trick is to first make the content take up the entire screen width, then start moving it.
Now our code for the closing animation looks like this:
Managing the layout and animation in React
So far we’ve been working with vanilla JS for the animation. However, if we’re working with a React component, we probably want to use React to manage the CSS classes and layout (in case your component does other things).
If you toggle open
rapidly in the above demo, you’ll notice that the background is sometimes visible again. This is because state updates with setContainerClassName
are asynchronous. React doesn’t update the container’s CSS class until the animation has already started.
This is where the state machine starts to come in. We can store the current state of the animation and use that to trigger the animation, rather than having the animation effect manage state.
The possible animation states here correspond to the state diagram above. Now that the animate state is stored in React state, it can be used to change the CSS class name when rendering.
Later on we’ll use the setter to mark when the animation is finished. Since we only want to set the 'open'
and 'closed'
states to indicate the animation is finished, we can restrict the possible parameter types with TypeScript. This helps enforce the paths in the state diagram.
With this change, the animation now starts at the same time as the CSS class name change! The state machine is now driving the layout (via React) and the animation (via useLayoutEffect
and the Web Animations API).
It’s also now possible to unmount the sidebar using React. Just like changing the class name, we can use the animation state to alter what items are rendered in sync with the animation.
With that, you have a complete and performant animation without the need for tools like react-spring or React Transition Group. You can check out the complete demo again here, and keep an eye out for this animation in Microsoft Loop.