A tutorial on how to create a Shazam-like button that morphs into a music player using Snap.svg.

Today we’d like to show you how to create a fun little morphing button effect. In this tutorial we’ll create a Shazam-like UI where we initially have a simple button that, when clicked, morphs into a listening button. We’ll animate some musical notes that fly from outside of the viewport to the listening button to indicate listening activity. Finally, the listening button will transform into a music player with album info of the “identified” song.

Note that we’ll not implement a music listening app but only the UI effects around the morphing button.

Attention: Some of the techniques we’ll be using are experimental and might not work in older browsers.

Planning the Effect

Our goal is to create a fluid morphing effect where we transform a button shape into a circle and then into a music player. For this, we could use plain HTML and CSS where we animate properties like the border radius, the width and the height. But this is not cheaply animated and it will not allow us to use any possible shape so we decided to use SVG for the shapes and animate them with Snap.svg.

The shapes that we’ll animate will only be used as background “decoration” meaning that our real elements will be overlaying them. The initial button, the listening button and the music player elements will be shown or hidden according to which step of the shape animation we’re at.

Since we’ll be animating SVG shapes, we need a way to store the path information of each shape. When morphing shapes with Snap.svg, we want to make sure that all shapes have the same amount of points so that the animation looks like a real “morph”.

So we need to keep that in mind when we draw our SVG in a program like Adobe Illustrator or Inkscape. We can’t use basic SVG shapes like a circle or a polygon but we need to use paths. A good way to ensure that we have the same amount of points is to first draw the most complex shape (i.e. the one with more points) and make sure that all the other shapes have that extra amount of points (even though they are not needed).

As you can see in the above image, there are a couple of more points than we actually need for the button shape. Those points will be needed for the more complex player path.

This is the SVG with all three shapes:

Besides controlling the morphing animation of the buttons and player, we want to create some musical notes and make them fly from outside the viewport towards the listening button.

For that we can simply create some elements that are positioned at the same place like the listening button. Then we translate those elements randomly outside of the viewport and animate them back to their origin.

The final step of our morphing animation will be the music player:

We will be using the following freely available design assets:

Using a service like IcoMoon or Fontastic we create web fonts out of the icons so that we can use them easily in our project.

So, let’s start writing the HTML.

The HTML

We need a main wrapper which will contain our SVG, the buttons and the player. That will be our division with the class “component”.

We’ll use some data attributes to save the starting path (initial button), the circular path (listening button) and the large rectangle (player). An SVG with the correct size and the same path as the initial button will serve as our drawing canvas where we’ll take those saved paths and replace the d that’s currently visible.

One button element will serve as a holder for the “start” and the “listen” button and then we build our player division that contains the album artwork, some meta info and some dummy player controls:

<div class="component" data-path-start="..." data-path-listen="..." data-path-player="..."> <!-- SVG with morphing paths and initial start button shape --> <svg class="morpher" width="300" height="500"> <path class="morph__button" d="..."/> </svg> <!-- Initial start button that switches into the recording button --> <button class="button button--start"> <span class="button__content button__content--start">Listen to this song</span> <span class="button__content button__content--listen"><span class="icon icon--microphone"></span></span> </button> <!-- Music player --> <div class="player player--hidden"> <img class="player__cover" src="img/Gramatik.jpg" alt="Water 4 The Soul by Gramatik" /> <div class="player__meta"> <h3 class="player__track">Virtual Insight</h3> <h3 class="player__album"> <span class="player__album-name">Water 4 The Soul</span> by <span class="player__artist">Gramatik</span> </h3> <div class="player__controls"> <button class="player__control icon icon--skip-back" aria-label="Previous song"></button> <button class="player__control player__control--play icon icon--play" aria-label="Play"></button> <button class="player__control icon icon--skip-next" aria-label="Next song"></button> </div> </div> <button class="button button--close"><span class="icon icon--cross"></span></button> </div><!-- /player --> </div><!-- /component -->

That’s all the HTML for now. Later on, we’ll also take care of the musical notes, but that we’ll do dynamically with JavaScript.

The CSS

So let’s begin by styling the main wrapper. We’ll give it the same dimensions like our SVG that we have designed earlier. The z-index is set to 1 so that we can ensure that other page elements are on top of our component, especially the musical notes that we’ll insert later:

.component { position: relative; z-index: 1; width: 300px; height: 500px; margin: 0 auto; }

The path that we’ll be always seeing will have a white fill:

.morph__button { fill: #fff; }

Next, we define the styles for our main button class. As you saw before, this class will be given to the starting and listening button and to the close button of our player.

While our SVG is left untouched for what concerns its position, all the other elements will be positioned absolutely, so that we can lay them over the shapes. So our button will be placed at the bottom of the component with the same width of the initial button path:

.button { font-weight: bold; position: absolute; bottom: 4px; left: 20px; width: calc(100% - 40px); height: 60px; padding: 0; text-align: center; color: #00a7e7; border: none; background: none; -webkit-transition: opacity 0.3s; transition: opacity 0.3s; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); } .button:hover, .button:focus { outline: none; color: #048abd; } .button--listen { pointer-events: none; } .button--close { z-index: 10; top: 0px; right: 0px; left: auto; width: 40px; height: 40px; padding: 10px; color: #fff; } .button--close:hover, .button--close:focus { color: #ddd; } .button--hidden { pointer-events: none; opacity: 0; }

This last class controls the visibility of the start/listen button.

Now, let’s see how we style the inner parts of the main button. Depending on which button content we want to show (the one of the start button or the one of the listen/microphone button), we toggle a class on the button.

The common style of the two types of content is the following:

.button__content { position: absolute; opacity: 0; -webkit-transition: -webkit-transform 0.4s, opacity 0.4s; transition: transform 0.4s, opacity 0.4s; }

By default, the content will be hidden and we add a transition so that we can fade and slide it a bit.

The content for the start button is a bit translated to the top:

.button__content--start { top: 0; left: 0; width: 100%; padding: 1.2em; text-indent: 1px; letter-spacing: 1px; -webkit-transform: translate3d(0, -25px, 0); transform: translate3d(0, -25px, 0); -webkit-transition-timing-function: cubic-bezier(0.8, -0.6, 0.2, 1); transition-timing-function: cubic-bezier(0.8, -0.6, 0.2, 1); }

The timing function allows a bit of a bouncing meaning that the “Listen to this song” text will first be pushed down a bit and then move up when it disappears.

And the content for the listening button is styled as follows:

.button__content--listen { font-size: 1.75em; line-height: 64px; bottom: 0; left: 50%; width: 60px; height: 60px; margin: 0 0 0 -30px; border-radius: 50%; -webkit-transform: translate3d(0, 25px, 0); transform: translate3d(0, 25px, 0); -webkit-transition-timing-function: cubic-bezier(0.8, 0, 0.2, 1); transition-timing-function: cubic-bezier(0.8, 0, 0.2, 1); }

We center the element by setting the left to 50% and pulling it back half of its own width with a negative margin. We’ll translate it down initially and adjust the timing function (we don’t want it to behave like the initial text when it slides in from the bottom).

Now, let’s take care of that little sonar ripple effect that happens when the microphone button appears.

For that, we’ll use the pseudo elements (::before and ::after) which we’ll position absolutely and style to be circles.

.button__content--listen::before, .button__content--listen::after { content: ''; position: absolute; left: 0; width: 100%; height: 100%; pointer-events: none; opacity: 0; border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 50%; }

The two rings will be animated:

.button--animate .button__content--listen::before, .button--animate .button__content--listen::after { -webkit-animation: anim-ripple 1.2s ease-out infinite forwards; animation: anim-ripple 1.2s ease-out infinite forwards; }

Let’s add a delay to one of them:

.button--animate .button__content--listen::after { -webkit-animation-delay: 0.6s; animation-delay: 0.6s; }

Next, we define the keyframes. What our animation does is fading the ring in and scaling it down; which makes it look like as if the ring is moving towards the button:

@-webkit-keyframes anim-ripple { 0% { opacity: 0; -webkit-transform: scale3d(3, 3, 1); transform: scale3d(3, 3, 1); } 50% { opacity: 1; } 100% { opacity: 0; -webkit-transform: scale3d(1, 1, 1); transform: scale3d(1, 1, 1); } } @keyframes anim-ripple { 0% { opacity: 0; -webkit-transform: scale3d(3, 3, 1); transform: scale3d(3, 3, 1); } 50% { opacity: 1; } 100% { opacity: 0; -webkit-transform: scale3d(1, 1, 1); transform: scale3d(1, 1, 1); } }

Back to our button content visibility. Finally, we define what’s shown when:

.button--start .button__content--start, .button--listen .button__content--listen { opacity: 1; -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); }

According to what we want to be visible, we set a class to the main button dynamically.

Next, let’s style the music player.

The main wrapper for the player is positioned absolutely at the same place like the matching path in our SVG:

.player { position: absolute; top: 10px; right: 10px; bottom: 10px; left: 10px; -webkit-transition: opacity 0.5s; transition: opacity 0.5s; }

We add a little gradient so that the album cover gets a bit darkened at the top so that we can see the white closing cross. Using the ::after pseudo-class, we create an overlay with the same height like the album cover:

.player::after { content: ''; position: absolute; top: -1px; /* for mobile Safari bug (white line of SVG visible) */ left: 0; width: 100%; height: 280px; pointer-events: none; border-radius: 5px 5px 0 0; background: linear-gradient(to bottom, rgba(0, 0, 0, 0.5), transparent); }

The visibility of the music player is controlled with the following class definition:

.player--hidden { pointer-events: none; opacity: 0; -webkit-transition: opacity 0.2s; transition: opacity 0.2s; }

Now, let’s style the inner parts of the player. This includes the album artwork, the meta info like the song title, album and band name, and the controls:

.player__cover { margin-top: -1px; /* for mobile Safari bug (white line of SVG visible) */ border-radius: 5px 5px 0 0; } .player__meta { padding: 0 1em 1em; text-align: center; } .player__track { font-size: 1.15em; margin: 1.25em 0 0.05em 0; color: #55656c; } .player__album { font-size: 0.825em; margin: 0; color: #bbc1c3; } .player__album-name, .player__artist { color: #adb5b8; } .player__controls { font-size: 1.15em; margin: 1.15em 0 0 0; } .player__control { margin: 0 0.85em; padding: 0; vertical-align: middle; color: #adb5b8; border: 0; background: none; } .player__control:hover, .player__control:focus { color: #00a7e7; outline: none; } .player__control--play { font-size: 1.75em; }

The last thing that we need to style are the notes. We’ll add those dynamically by inserting a division at the beginning of our component once we hit the start button. This division will be positioned absolutely at the bottom right where the listening button is:

.notes { position: absolute; z-index: -1; bottom: 0; left: 50%; width: 100px; height: 60px; margin: 0 0 0 -50px; }

Each note will also be positioned absolutely with the 50%/negative margin trick and we’ll give them a semi-transparent white color:

.note { font-size: 2.8em; position: absolute; left: 50%; width: 1em; margin: 0 0 0 -0.5em; opacity: 0; color: rgba(255, 255, 255, 0.75); }

Let’s change the look of some of the notes by using the nth-child selector and redefining the colors and font size:

.note:nth-child(odd) { color: rgba(0, 0, 0, 0.1); } .note:nth-child(4n) { font-size: 2em; } .note:nth-child(6n) { color: rgba(255, 255, 255, 0.3); }

And that’s all the styles. Now, let’s write some JavaScript!

The JavaScript

For the JavaScript part, we need to include Snap.svg, a custom Modernizr (open the file and see the features needed in the second line of the script), and classie.js for adding and removing classes. Let’s define our script that will take care of the morphing between the SVG shapes: the buttons and the audio player.

First, let’s define and initialize some variables.

// check support for CSS transitions var support = {transitions : Modernizr.csstransitions}, // prefixed name transEndEventNames = { 'WebkitTransition': 'webkitTransitionEnd', 'MozTransition': 'transitionend', 'OTransition': 'oTransitionEnd', 'msTransition': 'MSTransitionEnd', 'transition': 'transitionend' }, // transitionend event name transEndEventName = transEndEventNames[ Modernizr.prefixed( 'transition' ) ], // transitionend function onEndTransition = function( el, callback, propTest ) { var onEndCallbackFn = function( ev ) { if( support.transitions ) { if( ev.target != this || propTest && ev.propertyName !== propTest && ev.propertyName !== prefix.css + propTest ) return; this.removeEventListener( transEndEventName, onEndCallbackFn ); } if( callback && typeof callback === 'function' ) { callback.call(this); } }; if( support.transitions ) { el.addEventListener( transEndEventName, onEndCallbackFn ); } else { onEndCallbackFn(); } }, // the main component element/wrapper shzEl = document.querySelector('.component'), // the initial button shzCtrl = shzEl.querySelector('button.button--start'), // the svg element which contains the paths of the shapes shzSVGEl = shzEl.querySelector('svg.morpher'), // snap.svg instance snap = Snap(shzSVGEl), // the SVG path shzPathEl = snap.select('path'), // total number of notes/symbols moving towards the listen button totalNotes = 50, // the musical note elements notes, // the notes' speed factor relative to the distance from the note element to the button. // if notesSpeedFactor = 1, then the speed equals the distance (in ms) notesSpeedFactor = 4.5, // simulation time for listening (ms) simulateTime = 6500, // window sizes winsize = {width: window.innerWidth, height: window.innerHeight}, // button offset shzCtrlOffset = shzCtrl.getBoundingClientRect(), // button sizes shzCtrlSize = {width: shzCtrl.offsetWidth, height: shzCtrl.offsetHeight}, // tells us if the listening animation is taking place isListening = false, // audio player element playerEl = shzEl.querySelector('.player'), // close player control playerCloseCtrl = playerEl.querySelector('.button--close');

Next, let’s define our init function.

function init() { // create the music notes elements; the musical symbols // that will animate/move towards the listen button createNotes(); // bind events initEvents(); }

First we create the note elements’ structure and then we bind the necessary event listeners:

function createNotes() { var notesEl = document.createElement('div'), notesElContent = ''; notesEl.className = 'notes'; for(var i = 0; i < totalNotes; ++i) { // we have 6 different types of symbols (icon--note1, icon--note2 ... icon--note6) var j = (i + 1) - 6 * Math.floor(i/6); notesElContent += '<div class="note icon icon--note' + j + '"></div>'; } notesEl.innerHTML = notesElContent; shzEl.insertBefore(notesEl, shzEl.firstChild) // reference to the note elements notes = [].slice.call(notesEl.querySelectorAll('.note')); } function initEvents() { // click on the initial button shzCtrl.addEventListener('click', listen); // close the player view playerCloseCtrl.addEventListener('click', closePlayer); // window resize: update window sizes and button offset window.addEventListener('resize', throttle(function(ev) { winsize = {width: window.innerWidth, height: window.innerHeight}; shzCtrlOffset = shzCtrl.getBoundingClientRect(); }, 10)); }

We need to define what happens when we click the initial button, when we close the audio player (last step) and also when the window is resized.

Let's take care of what happens when the initial button is clicked:

function listen() { isListening = true; // toggle classes (button content/text changes) classie.remove(shzCtrl, 'button--start'); classie.add(shzCtrl, 'button--listen'); // animate the shape of the button // (we are using Snap.svg for this) animatePath(shzPathEl, shzEl.getAttribute('data-path-listen'), 400, [0.8, -0.6, 0.2, 1], function() { // ripples start... classie.add(shzCtrl, 'button--animate'); // music notes animation starts... showNotes(); // simulate the song detection setTimeout(showPlayer, simulateTime); }); }

We start by setting the isListening flag to true (used to control the notes animation loop), then we change the content of the button ("Listen to this song" becomes the microphone icon) and finally, the button shape path is morphed into the circular shape (the listening button with the microphone). Once the morphing is done, we start the ripples and the notes animation. Lastly, we wait for [simulateTime]ms to show the audio player.

Let's do the notes animation:

function showNotes() { notes.forEach(function(note) { // first, position the notes positionNote(note); // now, animate the notes towards the button animateNote(note); }); }

First we need to position the note elements randomly outside the viewport. Then we animate each note element toward the microphone element (the note's original position). Once the animation is done, the note gets repositioned again (randomly) and the animation is repeated. This cycle is repeated until isListening becomes false.

function positionNote(note) { // we want to position the notes randomly // (translation and rotation) outside of the viewport var x = getRandomNumber(-2*(shzCtrlOffset.left + shzCtrlSize.width/2), 2*(winsize.width - (shzCtrlOffset.left + shzCtrlSize.width/2))), y, rotation = getRandomNumber(-30, 30); if( x > -1*(shzCtrlOffset.top + shzCtrlSize.height/2) && x < shzCtrlOffset.top + shzCtrlSize.height/2 ) { y = getRandomNumber(0,1) > 0 ? getRandomNumber(-2*(shzCtrlOffset.top + shzCtrlSize.height/2), -1*(shzCtrlOffset.top + shzCtrlSize.height/2)) : getRandomNumber(winsize.height - (shzCtrlOffset.top + shzCtrlSize.height/2), winsize.height + winsize.height - (shzCtrlOffset.top + shzCtrlSize.height/2)); } else { y = getRandomNumber(-2*(shzCtrlOffset.top + shzCtrlSize.height/2), winsize.height + winsize.height - (shzCtrlOffset.top + shzCtrlSize.height/2)); } // first reset transition if any note.style.WebkitTransition = note.style.transition = 'none'; // apply the random transforms note.style.WebkitTransform = note.style.transform = 'translate3d(' + x + 'px,' + y + 'px,0) rotate3d(0,0,1,' + rotation + 'deg)'; // save the translation values for later note.setAttribute('data-tx', Math.abs(x)); note.setAttribute('data-ty', Math.abs(y)); } function animateNote(note) { setTimeout(function() { if(!isListening) return; // the transition speed of each note will be // proportional to its distance to the button // speed = notesSpeedFactor * distance var noteSpeed = notesSpeedFactor * Math.sqrt(Math.pow(note.getAttribute('data-tx'),2) + Math.pow(note.getAttribute('data-ty'),2)); // apply the transition note.style.WebkitTransition = '-webkit-transform ' + noteSpeed + 'ms ease, opacity 0.8s'; note.style.transition = 'transform ' + noteSpeed + 'ms ease-in, opacity 0.8s'; // now apply the transform (reset the transform so the note moves to its original position) and fade in the note note.style.WebkitTransform = note.style.transform = 'translate3d(0,0,0)'; note.style.opacity = 1; // after the animation is finished, var onEndTransitionCallback = function() { // reset transitions and styles note.style.WebkitTransition = note.style.transition = 'none'; note.style.opacity = 0; if(!isListening) return; positionNote(note); animateNote(note); }; onEndTransition(note, onEndTransitionCallback, 'transform'); }, 60); }

After [simulateTime]ms, the audio player is shown.

We need to "stop listening" and animate the microphone icon element into the player element shape. Again, we use Snap's animate function for that.

function showPlayer() { // stop the ripples and note animations stopListening(); // morph the listening button shape // into the audio player shape // we are setting a timeout so that there's // a small delay (it looks nicer) setTimeout(function() { animatePath(shzPathEl, shzEl.getAttribute('data-path-player'), 450, [0.7, 0, 0.3, 1], function() { // show audio player classie.remove(playerEl, 'player--hidden'); }); // hide button classie.add(shzCtrl, 'button--hidden'); }, 250); // remove this class so the button content/text gets hidden classie.remove(shzCtrl, 'button--listen'); }

The stopListening sets the isListening to false, stops the ripples animation and hides all note elements (fades them out).

function stopListening() { isListening = false; // ripples stop... classie.remove(shzCtrl, 'button--animate'); // music notes animation stops... hideNotes(); } function hideNotes() { notes.forEach(function(note) { note.style.opacity = 0; }); }

Once the player is shown, we can close it by clicking the top right cross button. Once again, we animate the player's shape into the initial button shape, and show the respective button content.

function closePlayer() { // hide the player classie.add(playerEl, 'player--hidden'); // morph the player shape into the initial button shape animatePath(shzPathEl, shzEl.getAttribute('data-path-start'), 400, [0.4, 1, 0.3, 1]); // show the button and its content again // we are setting a timeout so that there's a small delay (it looks nicer) setTimeout(function() { classie.remove(shzCtrl, 'button--hidden'); classie.add(shzCtrl, 'button--start'); }, 50); }

And that's it! The UI for our little Shazam button effect is done.

We hope you enjoyed this tutorial and learned a couple of interesting things in the process!