If you pay close attention to YouTube, you may have noticed that the video play button does not simply switch to a pause icon when clicked, but morphs very quickly between states. This is accomplished via , controlled with JavaScript. Here, I’ll show my variation of the UI pattern.

Making the Morph Targets

SVG morphs require that the “from” and “to” elements contain the same number of points. SVG is also limited in that it cannot “split” a closed element to morph into two separate paths. Therefore, making the play button in SVG means “hiding” the extra points needed for the pause state (8 points or more) inside the “play” button (which needs only 3 points). It also means making the play button in two separate parts, while appearing to be one object.

I made a triangle in on a 50 × 50 pixel artboard. Locking that drawing on its own layer, I built two separate elements on top, matching the shape:

The construction of the play element

Both elements overlap in the middle. Note that the right triangle has five points, rather than four, to retain the arrow shape; the rightmost three points are in separate positions, but they could be overlapped with each other.

Exported as SVG and with cleaned up code, the resulting markup looks like this:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" id="playpause" xmlns:xlink="http://www.w3.org/1999/xlink">
    <title>Play</title>
    <polygon points="12,0 25,11.5 25,39 12,50" id="leftbar" />
    <polygon points="25,11.5 39.7,24.5 41.5,26 39.7,27.4 25,39" id="rightbar" />
</svg>

We need to animate each <polygon> to its new state. By dragging around the existing points to form two vertical “bars”, I could export the result and add them into the previous SVG as values for <animate> elements. The SVG becomes:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" id="playpause" xmlns:xlink="http://www.w3.org/1999/xlink">
    <title>Play</title>
    <polygon points="12,0 25,11.5 25,39 12,50" id="leftbar" />
    <polygon points="25,11.5 39.7,24.5 41.5,26 39.7,27.4 25,39" id="rightbar" />
        <animate to="7,3 19,3 19,47 7,47" id="lefttopause" xlink:href="#leftbar" 
attributeName="points" dur=".3s" begin="indefinite" fill="freeze" />
        <animate to="31,3 43,3 43,26 43,47 31,47" id="righttopause" xlink:href="#rightbar" 
attributeName="points" dur=".3s" begin="indefinite" fill="freeze" />
</svg>

I’ve introduced two new attributes in this code since the previous SVG morphing article. begin="indefinite" stops the animation from executing immediately (if you wanted to test the animation, simply remove the attribute). fill="freeze" is equivalent to animation-direction: forward in CSS: it means that the animation plays through once, and stops at its final state. I’ve also placed the <animate> tags after the the polygons, rather than inside them, referencing the elements they influence via xlink:href values.

Once the SVG enters its pause state, we want to animate it back to play mode. Unfortunately, SVG doesn’t have a simple reverse mode; instead, we’ll create a second target state that returns the morphed elements back to their original form:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" id="playpause" xmlns:xlink="http://www.w3.org/1999/xlink">
    <title>Play</title>
    <polygon points="12,0 25,11.5 25,39 12,50" id="leftbar" />
    <polygon points="25,11.5 39.7,24.5 41.5,26 39.7,27.4 25,39" id="rightbar" />
        <animate to="7,3 19,3 19,47 7,47" id="lefttopause" xlink:href="#leftbar" 
attributeName="points" dur=".3s" begin="indefinite" fill="freeze" />
        <animate to="12,0 25,11.5 25,39 12,50" id="lefttoplay" xlink:href="#leftbar" 
attributeName="points" dur=".3s" begin="indefinite" fill="freeze" />
        <animate to="31,3 43,3 43,26 43,47 31,47" id="righttopause" xlink:href="#rightbar" 
attributeName="points" dur=".3s" begin="indefinite" fill="freeze" />
        <animate to="25,11.5 39.7,24.5 41.5,26 39.7,27.4 25,39" id="righttoplay" 
xlink:href="#rightbar" attributeName="points" dur=".3s" begin="indefinite" fill="freeze" />
</svg>

Note that I’ve provided each animation sequence with a separate id: lefttopause vs. lefttoplay, for example.

A Theatrical Display

The SVG is set inside a <button> element for easy access:

<div id="atlanticlight">
    <video controls>
        <source src="atlantic-light.webm">
        <source src="atlantic-light.mp4">
    </video>
    <button>
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" id="playpause" xmlns:xlink="http://www.w3.org/1999/xlink">
            <title>Play</title>
            <polygon points="12,0 25,11.5 25,39 12,50" id="leftbar" />
            <polygon points="25,11.5 39.7,24.5 41.5,26 39.7,27.4 25,39" id="rightbar" />
                <animate to="7,3 19,3 19,47 7,47" id="lefttopause" xlink:href="#leftbar" 
attributeName="points" dur=".3s" begin="indefinite" fill="freeze" />
                <animate to="12,0 25,11.5 25,39 12,50" id="lefttoplay" xlink:href="#leftbar" 
attributeName="points" dur=".3s" begin="indefinite" fill="freeze" />
                <animate to="31,3 43,3 43,26 43,47 31,47" id="righttopause" xlink:href="#rightbar" 
attributeName="points" dur=".3s" begin="indefinite" fill="freeze" />
                <animate to="25,11.5 39.7,24.5 41.5,26 39.7,27.4 25,39" id="righttoplay" 
xlink:href="#rightbar" attributeName="points" dur=".3s" begin="indefinite" fill="freeze" />
        </svg>
    </button>
</div>

Setting up includes setting the SVG to invisible by default; it will be made visible by the JavaScript to follow:

* { box-sizing: border-box; }
#atlanticlight { 
    position: relative;
    font-size: 0; width: 100%; 
}
#atlanticlight, #atlanticlight video, #atlanticlight button { 
    width: 100%; height: auto;
}
#atlanticlight button svg { 
    width: 50%; margin: 0 auto;
    fill: #fff; padding: 3rem;
    transition: .6s opacity;
}
#atlanticlight video, #atlanticlight button { 
    position: absolute; top: 0; 
}
#atlanticlight button { 
    background: transparent; 
    outline: none; border: none;
    cursor: pointer;
}
#playpause { display: none; }
.playing { opacity: 0; }

The transition on the SVG, applied via a class, will enable it to be faded in and out as we add and remove the class with classList.

Making Things Stop & Go

To make the play and pause button work, we first need to identify the elements we’re working on:

var atlanticlight = document.querySelector("#atlanticlight video"),
playpause = document.getElementById("playpause"),
lefttoplay = document.getElementById("lefttoplay"),
righttoplay = document.getElementById("righttoplay"),
lefttopause = document.getElementById("lefttopause"),
righttopause = document.getElementById("righttopause");

Then, using the principles of progressive enhancement, we remove the controls from the video, and display our own custom UI:

mountain.removeAttribute("controls");
vidcontrols.style.display = "block";

Finally, we listen to a click on the <button> element. Rather than tracking the state of the element, we look at the state of the video:

atlanticlight.removeAttribute("controls");
playpause.style.display = "block";
playpause.addEventListener(’click’,function(){
    if (atlanticlight.paused) {
        atlanticlight.play();
        playpause.classList.add("playing");
        lefttopause.beginElement();
        righttopause.beginElement();
    } else { 
        atlanticlight.pause();
        lefttoplay.beginElement();
        righttoplay.beginElement();
        playpause.classList.remove("playing");
    }
},false);

Future Perfect

If you look at Chrome’s console while running the example, above, you’ll read the following:

SVG’s SMIL animations (<animate>, <set>, etc.) are deprecated 
and will be removed. 
Please use CSS animations or Web animations instead.

For better or worse, this is true: Chrome will eventually abandon SMIL (the SVG-associated language we used to achieve the animation so far) in favor of the Web Animations API. Unfortunately, that spec doesn’t yet include the ability to morph, leaving this code in a difficult position over the long term, until a standalone JavaScript library is developed, or the Web Animations API expanded. In the meantime, we can use the >a href="https://greensock.com/">Greensock animation library as a fallback, which I will discuss in a future article.

Video by Peter Cox, licensed under Creative Commons

Enjoy this piece? I invite you to follow me at twitter.com/dudleystorey to learn more.
Check out the CodePen demo for this article at https://codepen.io/dudleystorey/pen/rLKBmw