We often see websites and apps that really get our attention throught some UI details and well designed animations. Those little animation pieces that complement the design are also known as micro-interactions, and they often makes the difference between a good experience and an awesome experience.

Today, we would like to show you how to build a functional download button, full of meaningful micro interactions. In the proccess, we will be learning some exciting things like:

advanced SVG path animations

how to perform multiple CSS animations sequencially

how we can glue together CSS and Javascript to get a fancy component

This will be a step by step tutorial, so you can learn all the implementation details involved. Let's start!

The original design of the download button we will build belongs to Pedro Aquino, and can be found on this Dribbble shot.

At the end of the tutorial, we will get a fancy download button like this:

To get it working, we will be using CSS transitions and animations, along with the lightweight animation library anime.js, and segment.js for SVG path animations.

For those in a rush, the full code can be found on this Github repository, and here is the demo page.

Please note that it's a bit complex component, that needs larger amount of code and consume more resources than a simple button, so be careful with performance in production. This is sometimes the cost to have fancy components like this in our apps, so it's a choice you need to make.

Upgrade Your JS Go from vanilla JavaScript 👉 React

Watch for FREE

So, let's see the HTML code we will be using:

< div class = " download-button-container " > < button class = " download-button " > < span class = " button-text-real hidden " > download </ span > < span class = " button-icon " > < span class = " button-linear-progress " > < span class = " button-linear-progress-bar " > </ span > </ span > < svg class = " button-icon-svg " viewBox = " 0 0 60 60 " > < path class = " button-icon-path button-icon-path-square " d = " M 20 40 l 0 -20 l 20 0 l 0 20 Z " > </ path > < path class = " button-icon-path button-icon-path-line " d = " M 40 20 l -20 20 " > </ path > </ svg > </ span > </ button > < svg class = " border-svg " width = " 240px " height = " 100px " viewBox = " 0 0 240 100 " > < path class = " border-path hidden " d = " M 40 3.5 a 36.5 36.5 0 0 0 -36.5 36.5 a 36.5 36.5 0 0 0 36.5 36.5 C 70 76.5 90 76.5 120 76.5 S 170 76.5 200 76.5 a 36.5 36.5 0 0 0 36.5 -36.5 a 36.5 36.5 0 0 0 -36.5 -36.5 Z " > </ path > </ svg > < span class = " button-text button-text-download " > download </ span > < span class = " button-text button-text-done " > done! </ span > < div class = " button-wave " > </ div > < div class = " button-progress-container " > < svg class = " button-svg " > < path class = " button-circular-progress " d = " M 50 50 m 0 -32.5 a 32.5 32.5 0 0 1 0 65 a 32.5 32.5 0 0 1 0 -65 " > </ path > </ svg > < span class = " button-ball " > </ span > </ div > </ div >

It is important to note that the SVG path elements have been drawn 'by hand' to get the result we want. For example, at some point, we want the button border to perform an elastic animation, so we need an SVG path ready for that morphing animation with anime.js (same structure in both paths ). Something like this:

With our markup ready, let's see some fundamental pieces of style needed to start getting the look and feel we are looking for. Please note that we are not including the whole stylesheet here to be able to remark only the main parts, but you can find the entire code on the Github repository. Also the code has been fully commented for better understanding.

So, let's see the SCSS variables we have defined, and a helper class to hide elements:

$button-width : 300 px ; $button-height : 70 px ; $button-border : 3 px ; $icon-padding : 5 px ; $icon-width : $button-height - ( $icon-padding * 2 ) ; $ball-width : 18 px ; .hidden { visibility : hidden !important ; opacity : 0 !important ; }

The styles for the real button element:

.download-button { position : relative ; display : inline-block ; width : $button-width ; height : $button-height ; background-color : #2C2E2F ; border : none ; box-shadow : 0 0 0 $button-border #02D1FF ; border-radius : 100 px ; cursor : pointer ; transition : 1 s width, 0.3 s box-shadow ; & , & :focus { padding : 0 ; outline : none ; } & ::-moz-focus-inner { border : 0 ; } & :hover, & :active, & :focus { box-shadow : 0 0 0 $button-border #02D1FF , 0 0 20 px $button-border darken ( #02D1FF , 20% ) ; } }

Our button could be in 3 different states: downloading , progressing and completed . So we have defined the styles needed for each state using the following structure:

.download-button-container { & .downloading { } & .progressing { } & .completed { } }

And another interesting piece of code, it's the used to achieve the ball animation when the download has finished:

.button-ball { left : 50% ; transition : none ; animation : ball-throw-up 0.5 s ease-out forwards, ball-throw-down 0.5 s 0.5 s ease-in forwards, ball-rubber 1 s forwards ; } @keyframes ball-throw-up { from { transform : translate ( - 50% , 17.5 px ) ; } to { transform : translate ( - 50% , - 60 px ) ; background-color : #00FF8D ; } } @keyframes ball-throw-down { from { transform : translate ( - 50% , - 60 px ) ; } to { transform : translate ( - 50% , 80 px ) ; } } @keyframes ball-rubber { from { width : $ball-width ; } 25% { width : $ball-width * 0.75 ; } 50% { width : $ball-width ; } to { width : $ball-width / 2 ; } }

As we have said before, all the other styles used can be found on the Github repository. We have just remark here the most exciting pieces of code used.

As we have pointed out at the beggining, we will be using anime.js and segment.js, both lightweight libraries to help with animations. You should have a basic understanding of them to continue, althought the code used is very straighforward.

Also please note that we will not include some variables declarations in the following code snippets, for the sake of clarity. If you have any doubth, please check the Github repository.

So, here is the basic code we are using to capture the click events on the button and perform the behavior we want:

button . addEventListener ( 'click' , function ( ) { if ( ! completed ) { if ( downloading ) { stopDownload ( ) ; } else { startDownload ( ) ; } } } ) ; function startDownload ( ) { downloading = true ; buttonContainer . classList . add ( 'downloading' ) ; animateIcon ( ) ; progressTimer = setTimeout ( function ( ) { buttonContainer . classList . add ( 'progressing' ) ; animateProgress ( ) ; } , 1000 ) ; } function stopDownload ( ) { downloading = false ; clearTimeout ( progressTimer ) ; buttonContainer . classList . remove ( 'downloading' ) ; buttonContainer . classList . remove ( 'progressing' ) ; stopProgress ( ) ; iconLine . draw ( 0 , '100%' , 1 , { easing : anime . easings [ 'easeOutCubic' ] } ) ; iconSquare . draw ( '30%' , '70%' , 1 , { easing : anime . easings [ 'easeOutQuad' ] } ) ; }

The animation progress has been faked in the demo, and for a real use case it should be replaced with real progress data. This is the function that handles the progress:

function animateProgress ( ) { circularProgressBar . draw ( 0 , '100%' , 2.5 , { easing : anime . easings [ 'easeInQuart' ] , update : updateProgress , callback : completedAnimation } ) ; }

Finally, here is the piece of code used to perform the animation when download has been completed, where the ball animation is triggered and we morph the path elements. Please follow the comments to not get confused in the callback hell :)

function completedAnimation ( ) { completed = true ; buttonContainer . classList . add ( 'completed' ) ; setTimeout ( function ( ) { button . classList . add ( 'button-hidden' ) ; ball . classList . add ( 'hidden' ) ; borderPath . classList . remove ( 'hidden' ) ; var morph = anime ( { targets : borderPath , d : 'M 40 3.5 a 36.5 36.5 0 0 0 -36.5 36.5 a 36.5 36.5 0 0 0 10.5 26.5 C 35 86.5 90 91.5 120 91.5 S 205 86.5 226 66.5 a 36.5 36.5 0 0 0 10.5 -26.5 a 36.5 36.5 0 0 0 -36.5 -36.5 Z' , duration : 100 , easing : 'linear' , complete : function ( ) { morph = anime ( { targets : borderPath , d : 'M 40 3.5 a 36.5 36.5 0 0 0 -36.5 36.5 a 36.5 36.5 0 0 0 36.5 36.5 C 70 76.5 90 76.5 120 76.5 S 170 76.5 200 76.5 a 36.5 36.5 0 0 0 36.5 -36.5 a 36.5 36.5 0 0 0 -36.5 -36.5 Z' , duration : 1000 , elasticity : 600 , complete : function ( ) { completed = false ; setTimeout ( function ( ) { buttonContainer . classList . remove ( 'completed' ) ; button . classList . remove ( 'button-hidden' ) ; ball . classList . remove ( 'hidden' ) ; borderPath . classList . add ( 'hidden' ) ; stopDownload ( ) ; } , 500 ) ; } } ) ; } } ) ; } , 1000 ) ; }

So far, we have seen the main pieces of code used to build this fancy download button:

As always, you can play with the live DEMO, or get the full code on Github. Please also note that this component is not fully ready for production, as it needs real progress data, maybe a failed state, etc.

A component like this require us to invest lot of time and effort, but as we are building something the final user would love, it totally worth it :)

We hope this tutorial has been useful, and we love to see you in the next one!

Like this article? Follow @lmgonzalves on Twitter