General purpose toolbar component that animates width, height or both.
Structure
This component consists of three main parts: the base, the content and
the container that wraps everything.
Base
The base is where all of the buttons live. This component persists
across states all of the states and it's very straightforward. I map
over the buttons, assign icons to each button and attach the correct onclick handler to expand the
corresponding content.
For the active state, I used an absolutely positioned div with a shared layoutId. This way the background
animates between buttons when changing the selected state.
These components are then used in a switch statement to render the
correct one based on the index. This would be a good place to use useMemo to optimize performance,
but I use the React compiler, so these get optimized automatically.
The content object is then used in a motion.div that handles the
animations through variants, assigns a key based on the index and
handles custom props like direction, isInitial and isCollapsing.
As outlined above, the animations are defined through variants. The main
properties animated are x and opacity. Custom props like direction, isInitial and isCollapsing are used to control
animation behavior.
direction makes this animation direction aware. I've previously struggled with
finding a way to make this direction aware in a way that makes sense and isn't
hacky, but Animations on the Web by Emil
Kowalski helped a lot.
isInitial determines whether the toolbar is collapsing from or to null and it prevents the motion.div from animating the
x value if that is the case.
isCollapsing is used to remove the container instantly when the toolbar is collapsing. This is done by
setting the duration of the opacity transition to 0.
The renderContent function is wrapped in AnimatePresence with direction, isInitial and isCollapsing passed through the
custom prop. All of this is placed in a div.
Animating height in Motion can get tricky at times if you decide to animate from and to auto.
To make sure both of the values I'm animating between are defined values, I used the useMeasure hook from useHooks
to calculate the height of the content.
const [ref, bounds] = useMeasure();
The ref is used on the content container to measure its height.
The measured value is then used in the height animation of the outer motion.div that wraps the content.
I separated the height of the content and the height of the base, which is why I also have to add the
BASE_HEIGHT to the measured value.
For animating width, you could use the exact same approach as for the height, as far as there is any content that defines the width of the
content container. However, for this example I just decided to go with fixed widths, that are assigned based on the index of the expanded
content.
With this setup the content can be anything and the component automatically calculates the correct height, although you might need to
adjust the transition settings based on the size.
Container
The container handles the enter/exit and height/width animations of the toolbar. The opacity, scale, translateY and
filter are all animated and used to create the toolbar appearing and disappearing animation.
In addition, width and height which were covered earlier in the article, are also animated as well as borderRadius.
In terms of the transition settings, I used different transition values for scale and translateY because the expanding
animation was a bit slower and more bouncy than I wanted the toolbar enter/exit animation to be.
Final Touches
Finally, the entire component is wrapped in AnimatePresence to handle the enter/exit animation of the toolbar itself.