Build a Download Button Full of Micro Interactions

Luis Manuel
👁️ 18,587 views
💬 comments

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!

Table of Contents

    What are we building?

    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:

    Fancy Download Button Demo

    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.

    HTML Structure

    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.

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

    <!-- Button container -->
    <div class="download-button-container">
        <!-- The real button -->
        <button class="download-button">
            <span class="button-text-real hidden">download</span>
            <!-- Extra elements to perform the animations -->
            <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>
        <!-- Extra elements to perform the animations -->
        <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:

    SVG paths for the download button border

    Adding Styles

    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:

    // Some variables to use later
    $button-width: 300px;
    $button-height: 70px;
    $button-border: 3px;
    $icon-padding: 5px;
    $icon-width: $button-height - ($icon-padding * 2);
    $ball-width: 18px;
    
    // Helper class to hide elements
    .hidden {
      visibility: hidden !important;
      opacity: 0 !important;
    }

    The styles for the real button element:

    // Real button styles
    .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; // This will be our 'border'
      border-radius: 100px;
      cursor: pointer;
      transition: 1s width, 0.3s box-shadow;
    
      // Remove the custom behavior in some browsers
      &, &:focus {
        padding: 0;
        outline: none;
      }
      &::-moz-focus-inner {
        border: 0;
      }
    
      // Styles for the different states of the button
      &:hover, &:active, &:focus {
        box-shadow: 0 0 0 $button-border #02D1FF, 0 0 20px $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:

    // Button container
    .download-button-container {
      // ...CODE...
    
      // Following are the different states for the button: downloading, progressing and completed
      // We have defined the states in the container to have access to all descendants in CSS
    
      // Downloading: The download button has been pressed
      &.downloading {
        // ...CODE...
      }
    
      // Progressing: The progress starts
      &.progressing {
        // ...CODE...
      }
    
      // Completed: The progress ends
      &.completed {
        // ...CODE...
      }
    }

    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;
      // CSS animations for the ball. All of them starts at the same time, so we need to take care of delays
      animation:
              ball-throw-up 0.5s ease-out forwards, // Throw up the ball for 0.5s
              ball-throw-down 0.5s 0.5s ease-in forwards, // Wait 0.5 seconds (throw up), and throw down the ball for 0.5s
              ball-rubber 1s forwards; // Move the ball like a rubber deformation during 1s (throw up + throw down)
    }
    
    // Throw up animation
    @keyframes ball-throw-up {
      from {
        transform: translate(-50%, 17.5px);
      }
      to {
        transform: translate(-50%, -60px);
        background-color: #00FF8D;
      }
    }
    
    // Throw down animation
    @keyframes ball-throw-down {
      from {
        transform: translate(-50%, -60px);
      }
      to {
        transform: translate(-50%, 80px);
      }
    }
    
    // Rubber animation
    @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.

    Bringing it to life with Javascript

    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:

    // Capture click events
    button.addEventListener('click', function () {
        if (!completed) { // Don't do anything if downloading has been completed
            if (downloading) { // If it's downloading, stop the download
                stopDownload();
            } else { // Start the download
                startDownload();
            }
        }
    });
    
    // Start the download
    function startDownload() {
        // Update variables and CSS classes
        downloading = true;
        buttonContainer.classList.add('downloading');
        animateIcon();
        // Update progress after 1s
        progressTimer = setTimeout(function () {
            buttonContainer.classList.add('progressing');
            animateProgress();
        }, 1000);
    }
    
    // Stop the download
    function stopDownload() {
        // Update variables and CSS classes
        downloading = false;
        clearTimeout(progressTimer);
        buttonContainer.classList.remove('downloading');
        buttonContainer.classList.remove('progressing');
        // Stop progress and draw icons back to initial state
        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:

    // Progress animation
    function animateProgress() {
        // Fake progress animation from 0 to 100%
        // This should be replaced with real progress data (real progress percent instead '100%'), and maybe called multiple times
        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 :)

    // Animation performed when download has been completed
    function completedAnimation() {
        // Update variables and CSS classes
        completed = true;
        buttonContainer.classList.add('completed');
        // Wait 1s for the ball animation
        setTimeout(function () {
            button.classList.add('button-hidden');
            ball.classList.add('hidden');
            borderPath.classList.remove('hidden');
            // Morphing the path to the second shape
            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 () {
                    // Morphing the path back to the original shape with elasticity
                    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 () {
                            // Update variables and CSS classes, and return the button to the original state
                            completed = false;
                            setTimeout(function () {
                                buttonContainer.classList.remove('completed');
                                button.classList.remove('button-hidden');
                                ball.classList.remove('hidden');
                                borderPath.classList.add('hidden');
                                stopDownload();
                            }, 500);
                        }
                    });
                }
            });
        }, 1000);
    }

    Conclusion

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

    Fancy Download Button Demo

    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!

    Luis Manuel

    12 posts

    Engineer in Computer Science and Freelance Front-End Developer. Available for Hire.