Animating an SVG Menu Icon with Segment

A tutorial on how to implement an animated menu icon based on the Dribbble shot by Tamas Kojo using SVG and Segment, a JavaScript library for drawing and animating SVG paths.

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:

Awesome Burguer Animation

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:

menuicon

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

Luis Goncalves

Front-end developer

Stay in the loop: Get your dose of frontend twice a week

πŸ‘Ύ Hey! Looking for the latest in frontend? Twice a week, we'll deliver the freshest frontend news, website inspo, cool code demos, videos and UI animations right to your inbox.

Zero fluff, all quality, to make your Mondays and Thursdays more creative!

Feedback 33

Comments are closed.
    • 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 πŸ˜‰

  1. 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!

    • 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.

  2. 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 define 100% + 1 instead of 100%.
      Any other problem, we can check it on github. Thanks!

  3. 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

  4. Nice work! completely failed at getting it to work though. can’t seem to get segment to draw anything :/

  5. 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.

  6. 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!

  7. 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.

  8. Hi, Would you mind telling me how you created the paths? I mean did you use illustrator or anything ?

  9. 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!

  10. 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.