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;
}

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

Tagged with:

Luis Goncalves

Front-end developer

Stay up to date with the latest web design and development news and relevant updates from Codrops.