From our sponsor: Chromatic - Visual testing for Storybook, Playwright & Cypress. Catch UI bugs before your users do.
Today we are very happy to share an interesting menu icon effect with you. The idea is based on the Dribbble shot hamburger menu by Tamas Kojo. At first, the icon is the classic burger menu icon. But when you click on it, it becomes a close icon with a fun “ninja” effect. The animation is reversed when you click on the close icon which turns it into the initial hamburger icon again. Take a look:
In this tutorial we are going to recreate this effect using SVG and a new library called Segment. First, we will do some initial planning, then we’ll introduce Segment a bit and later on we will draw and animate our hamburger icon.
Planning
To achieve this effect, I cannot imagine anything better than SVG. And the new library Segment (which is an alternative to the DrawSVGPlugin from GSAP) provides the necessary utilities to implement it.
The main idea is to create three paths that describe the trajectory of each bar on the burger icon when it transforms to the close icon. The Segment library will allow us to animate the path strokes in the way we want. To draw paths, any vector editor (like Adobe Illustrator or Inkscape) can be used; in this case we’ll be drawing the paths manually (tying lines, curves and arcs), because we want to get the best possible accuracy. Keep in mind that we are doing an animation that contains “elastic” movements, therefore these must be considered in the length of each path. But before we continue, let’s have a look at Segment.
Introducing Segment
The main tool we’ll be using is Segment, a little JavaScript class (without dependencies) for drawing and animating SVG path strokes. Using Segment is pretty straightforward:
<!-- Add the segment script (less than 2kb) -->
<script src="/dist/segment.min.js"></script>
<!-- Define a path somewhere -->
<svg>
<path id="my-path" ...>
</svg>
<script>
// Initialize a new Segment with the path
var myPath = document.getElementById("my-path"),
segment = new Segment(myPath);
// Draw a segment of a stroke at the time you want
// Syntax: .draw(begin, end[, duration, options])
segment.draw("25%", "75% - 10", 1);
/* Full example with all possible options */
// Define a normalized easing function (t parameter will be in the range [0, 1])
function cubicIn(t) {
return t * t * t;
}
// Define a callback function
function done() {
alert("Done!");
}
// Draw the complete path
segment.draw(0, "100%", 1, {delay: 1, easing: cubicIn, callback: done});
</script>
To learn more you can play with the demo and check out the documentation on GitHub. Also, if you want to understand how Segment works, you can read more about it in this article.
It is important to note that Segment does not include any easing function (except the default linear one), so we will be using the excellent d3-ease library for this.
Drawing
It’s a very quick animation, but if we analyze the animation frame by frame, we can draw each path. The result is something like this:
Created from the following code we’ve developed piece by piece:
<svg width="100px" height="100px">
<path d="M 30 40 L 70 40 C 90 40 90 75 60 85 A 40 40 0 0 1 20 20 L 80 80"></path>
<path d="M 30 50 L 70 50"></path>
<path d="M 70 60 L 30 60 C 10 60 10 20 40 15 A 40 38 0 1 1 20 80 L 80 20"></path>
</svg>
Now we need to add the proper CSS styles to the paths to achieve the desired effect, and an id
to access them easily from our script. This is the HTML structure we’ll be using:
<!-- Wrapper -->
<div id="menu-icon-wrapper" class="menu-icon-wrapper">
<!-- SVG element with paths -->
<svg width="100px" height="100px">
<path id="pathA" d="M 30 40 L 70 40 C 90 40 90 75 60 85 A 40 40 0 0 1 20 20 L 80 80"/>
<path id="pathB" d="M 30 50 L 70 50"/>
<path id="pathC" d="M 70 60 L 30 60 C 10 60 10 20 40 15 A 40 38 0 1 1 20 80 L 80 20"/>
</svg>
<!-- Trigger to perform the animations -->
<button id="menu-icon-trigger" class="menu-icon-trigger"></button>
</div>
And the CSS styles:
// The wrapper was defined with a fixed width and height
// Note, that the pointer-events property is set to 'none'.
// We don't need any pointer events in the entire element.
.menu-icon-wrapper{
position: relative;
display: inline-block;
width: 34px;
height: 34px;
pointer-events: none;
transition: 0.1s;
}
// To perform the scaled transform for the second demo
.menu-icon-wrapper.scaled{
transform: scale(0.5);
}
// Adjusting the position of the SVG element
.menu-icon-wrapper svg{
position: absolute;
top: -33px;
left: -33px;
}
// Defining the styles for the path elements
.menu-icon-wrapper svg path{
stroke: #fff;
stroke-width: 6px;
stroke-linecap: round;
fill: transparent;
}
// Setting the pointer-events property to 'auto',
// and allowing only events for the trigger element
.menu-icon-wrapper .menu-icon-trigger{
position: relative;
width: 100%;
height: 100%;
cursor: pointer;
pointer-events: auto;
background: none;
border: none;
margin: 0;
padding: 0;
}
Tiny break: 📬 Want to stay up to date with frontend and trends in web design? Subscribe and get our Collective newsletter twice a tweek.
Animating
With the SVG code ready, our task now is to figure out or to guess the easing functions used in each section of the animation, and to achieve a proper synchronization, always guided by the animated GIF. Let’s see how to animate the top and bottom bars of the hamburger icon. First, we need to initialize a segment for each bar with the initial begin
and end
values. Because we don’t have the information at hand but only the visual animation of the GIF, this is a trial and error process until we find the right values.
var pathA = document.getElementById('pathA'),
pathC = document.getElementById('pathC'),
segmentA = new Segment(pathA, 8, 32),
segmentC = new Segment(pathC, 8, 32);
With that we are ready to animate, always keeping the same length (end - begin = 24
) during the whole animation. Analyzing the animation sequence, we can see that the first part starts with a linear easing function, and ends with an elastic one. We’ll be using functions that receive the segment
as a parameter to reuse the same function with the top and bottom bars, because they will be animated in the same way.
// Linear section, with a callback to the next
function inAC(s) { s.draw('80% - 24', '80%', 0.3, {delay: 0.1, callback: function(){ inAC2(s) }}); }
// Elastic section, using elastic-out easing function
function inAC2(s) { s.draw('100% - 54.5', '100% - 30.5', 0.6, {easing: ease.ease('elastic-out', 1, 0.3)}); }
// Running the animations
inAC(segmentA); // top bar
inAC(segmentC); // bottom bar
We just need to repeat the same process for the middle bar:
// Initialize
var pathB = document.getElementById('pathB'),
segmentB = new Segment(pathB, 8, 32);
// Expand the bar a bit
function inB(s) { s.draw(8 - 6, 32 + 6, 0.1, {callback: function(){ inB2(s) }}); }
// Reduce with a bounce effect
function inB2(s) { s.draw(8 + 12, 32 - 12, 0.3, {easing: ease.ease('bounce-out', 1, 0.3)}); }
// Run the animation
inB(segmentB);
To reverse the animation back to the hamburger icon we’ll be using:
function outAC(s) { s.draw('90% - 24', '90%', 0.1, {easing: ease.ease('elastic-in', 1, 0.3), callback: function(){ outAC2(s) }}); }
function outAC2(s) { s.draw('20% - 24', '20%', 0.3, {callback: function(){ outAC3(s) }}); }
function outAC3(s) { s.draw(8, 32, 0.7, {easing: ease.ease('elastic-out', 1, 0.3)}); }
function outB(s) { s.draw(8, 32, 0.7, {delay: 0.1, easing: ease.ease('elastic-out', 2, 0.4)}); }
// Run the animations
outAC(segmentA);
outB(segmentB);
outAC(segmentC);
Finally, in order to perform the respective animation with the a click event, we can do something like this:
var trigger = document.getElementById('menu-icon-trigger'),
toCloseIcon = true;
trigger.onclick = function() {
if (toCloseIcon) {
inAC(segmentA);
inB(segmentB);
inAC(segmentC);
} else {
outAC(segmentA);
outB(segmentB);
outAC(segmentC);
}
toCloseIcon = !toCloseIcon;
};
The animation is complete, but there is a little problem. It does not look exactly the same in all browsers. The path lengths seem to be calculated slightly different and so there is a small (but significant) difference, mainly between Firefox and Chrome. How do we fix it?
The solution is quite simple. We can simply make our SVG larger so that the paths are much longer, and then resize or scale down to the desired dimensions. In this case we have resized our SVG drawing to be 10 times larger than before, so we have the following code:
<svg width="1000px" height="1000px">
<path id="pathA" d="M 300 400 L 700 400 C 900 400 900 750 600 850 A 400 400 0 0 1 200 200 L 800 800"></path>
<path id="pathB" d="M 300 500 L 700 500"></path>
<path id="pathC" d="M 700 600 L 300 600 C 100 600 100 200 400 150 A 400 380 0 1 1 200 800 L 800 200"></path>
</svg>
Then we have scaled down to the original dimensions with CSS:
.menu-icon-wrapper svg {
transform: scale(0.1);
transform-origin: 0 0;
}
Note that we also need to increase the float values in the JavaScript code (multiply by ten) and we’ll have to adjust the stroke-width
attribute in the CSS. If you don’t mind very small cross-browser differences then you can also stick to the original size, but this workaround might help you troubleshoot some differences.
Today we’ve explored how to use the Segment library to achieve an elastic SVG animation. This is one of the possible ways to achieve this kind of effect. Now it’s your turn to do some creative SVG animations π
We hope you enjoyed this tutorial and find it useful!
Browser Support:- ChromeSupported
- FirefoxSupported
- Internet ExplorerSupported from version 9+
- SafariSupported
- OperaSupported
Soo smooth. Really great one.
That’s gorgeous. I made an implementation using Popmotion
That’s an awesome animation. Thank you very much for sharing. π
Demo shows nothing, using latest chrome
Good job. It’s pretty smooth and it looks nice.
There’s also http://lcdsantos.github.io/jquery-drawsvg/ as a Segment alternative.
Thanks for your words π
But jquery-drawsvg is not like Segment. For example, I think you can’t do this kind of animations with it. It just solve another set of problems π
Have you been thinking about video tutorials?
Useful resource i will try to implement it on my next project.
Hi, Luis!
Maybe it’s a stupid question, but I can’t change width and height of hamburger icon (by default it 34 x 34 pixels): it is possible to set custom values and how?
Thanks in advance!
Mistake: default dimensions 38 x 38 pixels…
Hi, thanks for comment!
I think the easiest way is setting scaling transformation, like:
.menu-icon-wrapper { transform: scale(0.8); /* Don't forget prefixes */ }
Maybe you can do it with the
viewbox
attribute. Read this article for more information.Wow! Thanks for detailed explanation of every step!
Nice π
It Works at Blackberry Browsers?
This is a great UX booster. Gonna definitely start using this library. Love the details. Thx!
Love it! π
The libraby itself is awesome, but i have some issues trying to make a self writing text with a clipPath on top of it. The animation in Chrome stumbles. And the problem occurs only in Chrome! Even IE plays it perfect. I’ve tried to do the trick with the bigger paths and it didn’t help a bit. The easing libraby helped a little and it looks smoother now but not like in firefox. And my other problem is that when i set the end point of the animation to 100% sometimes it doesn’t play all the way. Any suggestions? Thank you and have a great day!
Hi Matt, thanks for comment! If you have any issue related with the Segment library, the best thing you can do is to open a new issue on github (maybe with a demo showing your problem). I’ll check it as soon as possible π
On the other hand, it has also happened to me that sometimes the stroke does not reach
100%
. I still don’t know why it happens (sometimes), but a workaround can be to define100% + 1
instead of100%
.Any other problem, we can check it on github. Thanks!
This tutorial is so cool , and i just used it in one of my project , but the problem is that when i tried to upgrade , the code broke because the d3.ease plugin is not using maps so this code will break ease.ease(‘elastic-out’, 1, 0.3)}) since there is not more the ease function and also the ease global is replaced with the d3_ease
Yeah, this uses v0.1.5 of d3-ease .
A lot has changed since then.
Nice work! completely failed at getting it to work though. can’t seem to get segment to draw anything :/
Hi, Luis Manuel.
I have a problem with the code.
The div is above all, and if we any other link (text or button), in the html that is below can not be used (the div #dummy.dummy is a invisible mask for all this).
How can this be solved?
Greetings from Spain.
Hi, thanks a lot for this great article! I made an implementation of the icon using Vue JS. Little trick for those where ease wouldn’t work as expected using the npm package, I had to switch to the 0.1.5 as they made some changes implementing their functions. Anyway, thanks a lot, this looks great!
I rarely comments on any article but after seeing your work i cant stop my self to say you thank you and keep doing that great work. Its my first time on that website and i love just this website. BOOKMARK it.
Grate
It’s very very nice and fantastic…but please make videos in YouTube….
Hi, Would you mind telling me how you created the paths? I mean did you use illustrator or anything ?
Yes, I’ve used Inkscape (another vector graphics software) at first. But then it was necessary to build the paths “by hand”, for maximum possible accuracy.
This article was really helpful for me: https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths
I have a problem,please see the link blow:
https://stackoverflow.com/questions/44255917/jquery-rain-animated-menu-icon-onclick-not-working
I’ve tried to achieve similar effect using only css and neither always I can use js in my projects
if someone wants to improve it, please notify me, I would like to see.
https://codepen.io/wesleyguirra/pen/QMaPdY
Realy nice effect ! Thank you for the tutorial !
Hi Luis,
Great tutorial! Unfortuantely, I have no idea how to make the “Close” animation when clicking on whatever it opened (a menu in this case). Could you please show me?
thank you!
Menu looks cool. But when I add links(anchor tag) to menu items and click at the place of any Menu item after closing of menu, menu items are working!!!
Please fix it.