51
Animate a Hamburger Menu with Framer Motion
Welcome back to my backyard lab, where I do my experiments drinking a lot of coffee to keep the pace π
Currently, in my spare time, I'm working on my personal website and I want to share with you my process on how I built and animate the hamburger menu of the header.
Animations are cool!! So why not complicate your life by animating some sections of your personal website? There is no better place to do it...
But...since I'm not an animation Guru, I asked for help from Framer Motion, a React animation library by Framer.
My animation is pretty simple, the SVG has two lines (one is wider), on click/tap, the shorter one stretches reaching the max length and then I rotate both lines and create the X shape.
In this article I will show you 2 solutions that I implemented, called respectively:
- 'I don't know Framer Motion' solution (a.k.a. It works solution).
- '(Maybe) I know Framer Motion a little bit more' solution.
Since I want to use SVG, firstly I created 3 shapes in Figma, representing my animation states. Every shape is inside a 24 x 24 box.
The first shape represent the 'closed' state:
The second, represent the middle state:
The last one, represent the 'open' state, when animation is completed:
The following are the 3 SVG exported from Figma.
<!-- CLOSED STATE -->
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<line y1="9.5" x2="24" y2="9.5" stroke="#FFFFFF"/>
<line y1="14.5" x2="15" y2="14.5" stroke="#FFFFFF"/>
</svg>
<!-- MIDDLE STATE -->
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<line y1="9.5" x2="24" y2="9.5" stroke="#FFFFFF"/>
<line y1="14.5" x2="24" y2="14.5" stroke="#FFFFFF"/>
</svg>
<!-- OPEN STATE -->
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.06061 2.99999L21.0606 21" stroke="#FFFFFF"/>
<path d="M3.00006 21.0607L21 3.06064" stroke="#FFFFFF"/>
</svg>
The first problem I faced was about the line tags used in the first two SVG and the path used on the third.
So I decided to align everything with the path tag and I used this 'formula' that I found online:
d="Mx1 y1Lx2 y2"
So the first 2 SVG have become:
<!-- CLOSED STATE -->
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 9.5L24 9.5" stroke="#FFFFFF"/>
<path d="M0 14.5L15 14.5" stroke="#FFFFFF"/>
</svg>
<!-- MIDDLE STATE -->
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 9.5L24 9.5" stroke="#FFFFFF"/>
<path d="M0 14.5L24 14.5" stroke="#FFFFFF"/>
</svg>
So, now that every SVG is aligned, I can start tuning the variants needed by Framer Motion in order to work.
This is the top line/path. This line doesn't have a middle state so the object represents only the initial and final state.
const path01Variants = {
open: { d: 'M3.06061 2.99999L21.0606 21' },
closed: { d: 'M0 9.5L24 9.5' },
}
This line is the bottom one and since it is the only one that has a middle state, the following object contains three keys/states.
const path02Variants = {
open: { d: 'M3.00006 21.0607L21 3.06064' },
moving: { d: 'M0 14.5L24 14.5' },
closed: { d: 'M0 14.5L15 14.5' },
}
Okay...what I've to do is pretty straightforward:
- click on the SVG
- start the animation
- repeat!
The problem I faced with variants and the animate property was that I was not able to create a sequence between animation states.
I couldn't start from 'closed', switch to 'moving' and finally reach the 'open' state.
Since I'm using React, I thought that my component state could have more than a boolean-like (open/close) value so I created an 'animation' state with 3 possible values: 'closed', 'moving' and 'open'.
But how to sequence them? Well...a good old setTimeout came to my rescue...
const [animation, setAnimation] = useState('closed');
const onClick = () => {
setAnimation('moving');
setTimeout(() => {
setAnimation(status === 'closed' ? 'open' : 'closed');
}, 200);
};
and my return statement is:
<button onClick={onClick}>
<svg width='24' height='24' viewBox='0 0 24 24'>
<motion.path
stroke='#FFFFFF'
animate={animation}
variants={path01Variants}
/>
<motion.path
stroke='#FFFFFF'
animate={animation}
variants={path02Variants}
/>
</svg>
</button>
Here the animate={animation}
changes its state with the value inside my React state and then the proper variant inside 'pathXXVariants' is used.
I start with the 'moving' state and after 200ms I switch to open or closed (it depends on the previous state).
The previous solution works, but I find the 'setTimeout' as a loophole to do something that probably the library can handle in some different way and most important, even if probably this is not the case, using setTimeout and React come with some caveats so I could have used also something more 'React' as well.
Anyway, diving into the documentation a little bit more I found a React hook that could be helpful for my scenario.
The useAnimation hook creates an AnimationControls
object that has some utility methods which I can use to fine-tune my animation.
With AnimationControls I can start an animation and since this method returns a Promise I can also await that the animation ends and start the following one. So as you can imagine we have more control π.
I changed the React state back to a boolean and created 2 AnimationControls, one for each path.
const [isOpen, setOpen] = useState(false);
const path01Controls = useAnimation();
const path02Controls = useAnimation();
our onClick handler now is a little bit more complicated but more Framer Motion friendly:
const onClick = async () => {
// change the internal state
setOpen(!isOpen);
// start animation
if (!isOpen) {
await path02Controls.start(path02Variants.moving);
path01Controls.start(path01Variants.open);
path02Controls.start(path02Variants.open);
} else {
path01Controls.start(path01Variants.closed);
await path02Controls.start(path02Variants.moving);
path02Controls.start(path02Variants.closed);
}
};
I decided to tune the duration of the animation directly on the JSX but I could have also inserted it as the second argument of the start method or in the variant itself.
So the final JSX...
<button onClick={onClick}>
<svg width='24' height='24' viewBox='0 0 24 24'>
<motion.path
{...path01Variants.closed}
animate={path01Controls}
transition={{ duration: 0.2 }}
stroke='#FFFFFF'
/>
<motion.path
{...path02Variants.closed}
animate={path02Controls}
transition={{ duration: 0.2 }}
stroke='#FFFFFF'
/>
</svg>
</button>
and here a little demo π
This is it!
I hope you enjoyed this little tutorial and if you found it helpful, drop a like or a comment.
If you know Framer Motion and you want to share your thoughts or an alternative/better solution...please do it!!!
If you think that Framer Motion is an overkill for this little animation...yeah you are probably right but it was fun learning something new π.
Thanks for reading!
See ya π€
51