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:
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.
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
):
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.
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);
}
This article showed the main pieces of code used to build this download button:
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.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
While we believe that this content benefits our community, we have not yet thoroughly reviewed it. If you have any suggestions for improvements, please let us know by clicking the “report an issue“ button at the bottom of the tutorial.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
Sign up for Infrastructure as a Newsletter.
Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.
Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.