How To Build a Download Button with Microinteractions with CSS, anime.js, and segment.js

PostedDecember 12, 2019 673 views JavaScript

While this tutorial has content that we believe is of great benefit to our community, we have not yet tested or edited it to ensure you have an error-free learning experience. It's on our list, and we're working on it! You can help us out by using the "report an issue" button at the bottom of the tutorial.

Introductions

In user experience design, microinteractions are small moments of feedback that help a user navigate an interface. Often, microinteractions are made with subtle animation in website design.

In this tutorial, you will build a functional download button with microinteractions. 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.

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

Fancy Download Button Demo

The original design of the download button belongs to Pedro Aquino, and can be found on this Dribbble shot. The full code can be found on this Github repository, and here is the demo page.

Step 1 — Making the HTML Structure

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):

SVG paths for the download button border

Step 2 — Adding Styles

With our markup ready, let’s style our button. Please note that we are not including the whole stylesheet here, but rather the most important parts; you can find the entire code on the Github repository. The code has been fully commented for better understanding.

Let’s see the SCSS variables we have defined, and the 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 can be in three different states: downloading, progressing, and completed. We therefore 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...
  }
}

Another interesting piece of code is 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 start 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;
  }
}

All the other styles used can be found on the Github repository.

Step 3 — Animating with Javascript

We will be using anime.js and segment.js, both lightweight libraries to help with animations.

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

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; for a real use case it will 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.

// 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

This article showed the main pieces of code used to build this download button:

Fancy Download Button Demo

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 and some considerations on how the backend will affect the microinteractions.

0 Comments

Creative Commons License