From our sponsor: Ready to show your plugin skills? Enter the Penpot Plugins Contest (Nov 15-Dec 15) to win cash prizes!
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.
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:
- Musical notes icons made by SimpleIcon from www.flaticon.com, licensed under CC BY 3.0
-
Feather icons by Cole Bemis licensed under MIT
-
Octicons by GitHub
-
Background pattern designed by Freepik
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.
Tiny break: 📬 Want to stay up to date with frontend and trends in web design? Subscribe and get our Collective newsletter twice a tweek.
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!
Browser Support:- ChromeSupported
- FirefoxExperimental flag
- Internet ExplorerSupported from version 11+
- SafariSupported
- OperaSupported
Love it, thanks Mary Lou!
Its superb, Thanks Mary. 🙂
Is there any program where I can make a picture like the one they used for background.
one that can be repeated and take all the time the same form
thanks so
@Georg – yes, you can do that with Photoshop and you want to learn how to create patterns (look up for tutorials).
Also, you can use a website that has patterns like subtlepatterns.com.
Very cool..
However are the SVG shapes needed? Couldn’t these have just been created through CSS properties and CSS animations?
Great effect. How to embed a mp3 file ?
Hi, Mary! Good new works! And can you tell me how to do the effects of loading? I just do not quite understand how the loading
First, to be a picture in gife or to CSS
Then – the site should be shown
I have yet shown any image or website
Thank you in advance!
I really like the design feature of this effect! The coding seems fairly manageable but for some might be on that border where it could be better to look to a professional, at least for some advice or assistance..
Great! ´m a big fan!
wow..! Its really cool..
Awesome, Its very nice and superb…
Thanks for sharing dude. It is very useful to me..
Very creative design here i can see. This looks very creative with blue background. And the important thing is morphing button effect.
Great! Its really good
Is there any reason you use :
var j = (i + 1) – 6 * Math.floor(i/6);
instead of :
var j = (i + 1);
?
Oh I understand now, no need to approve previous comment.
But I think “var j = i%6+1;” is easier to understand.
That’s what I’m looking for. I love Shazam design.
gotta find me a developer to implement this. I love it
Thank you for your effort. That’s great. I really like.