Easy micro-interactions in CSS (Pt2): Multi-state button
Anastasia Kas
Posted on February 8, 2020
A little preface, this is the second part of my micro-interactions in CSS posts (or micro-tutorials, even). And I am aware it has taken ages for me to finally get to the second part, but life has been crazy!
The last part was devoted to the star of all responsive websites and web apps - hamburger icon.
If you're not particularly interested in hamburger icons, let's just recap what a micro-interaction is (a bit of poetry, well, almost):
Microinteractions are an exercise in restraint, in doing as much as possible with as little as possible. Embrace the constraints and focus your attention on doing one thing well. β Dan Saffer
As you may have guessed from the title, this part is going to be about buttons - something no site or app can do without. The button is also one of the oldest elements in UI that has gone through a lot of visual transformations over the years, starting with a simple square shape and a small shadow, going through the 3d phase, flat design age, and now the material design look. The modern frameworks also enabled the use of a button as a status communicator, and this is precisely the use cases we'll be looking at.
Just as a reminder, I'm using Slim and SCSS, but you can always compile and grab HTML and CSS from the codepen examples!
Multi-state button
As implementing such a button takes more effort and essentially all of the other use cases are similar in terms of state switching (Eg. Download -> Downloading -> Done or Netflix-style skip: Disabled -> Loading -> Skip), we'll only go through one example. For this example, let's focus on a message or email send button.
Let's start by adding 3 possible states' text values into DOM and then manipulating their visibility and positioning. In most cases, you will also need a 4th state for error, but to simplify this example, I won't be doing that.
So let's have a closer look at what we need to do.
- Adding an icon element
- Adding 3 text values
- Providing 3 styles for the button background-color
- Showing separate icon, switching text value and background depending on the state
The structure of the button is as follows:
button.button.button--default
.button__icon-wrapper
.button__icon
.button__text-wrapper
.button__text.button__text--default Send
.button__text.button__text--process Cancel
.button__text.button__text--success Sent!
And the basic styling (you can look up color names here):
.button {
font-size: 1.2em;
text-align: left;
padding: 0.8em 1.4em;
border-radius: 2em;
cursor: pointer;
font-family: 'Montserrat', sans-serif;
font-weight: 600;
border: 0;
color: WHITE;
line-height: 1.6em;
transition: background-color 300ms ease-in-out;
&:focus {
outline: 0;
}
&--default {
background-color: CORNFLOWERBLUE;
}
&--process {
background-color: MEDIUMSLATEBLUE;
}
&--success {
background-color: DARKTURQUOISE;
}
}
Lastly, let's add a simple script to toggle classes onClick. In a real-world application, you'd want the class toggle to be triggered by other events and most likely in a different manner altogether, but for now, we'll just use setTimeout()
.
const button = document.querySelector('.button');
button.addEventListener('click', function(){
button.classList.remove('button--default');
button.classList.add('button--process');
setTimeout(function() {
button.classList.remove('button--process');
button.classList.add('button--success');
}, 2000);
setTimeout(function() {
button.classList.remove('button--success');
button.classList.add('button--default');
}, 4500);
})
At this point, you should have a button with vertically stacked text, which onClick
will start changing colors (and classes):
Now it's time to manipulate text switching, we'll do so by absolute positioning every element inside .button__text__wrapper
and toggling their opacity and transformation depending on the currently active class.
The default will be opacity: 0;
, and transform: translateY(20px);
, this will give us a nice fade-from-the-bottom animation (see next paragraph).
&__text-wrapper {
position: relative;
display: inline-block;
width: 120px;
height: 1.6em;
vertical-align: top;
}
&__text {
position: absolute;
opacity: 0;
transform: translateY(20px);
transition: all 250ms ease-in-out;
}
It's time to apply transformation depending on the active class inside our .button
, we'll need to transform the needed text to transform: translateY(0);
and set opacity: 1;
, this alone will give us a nice enough fade-in effect. But it's much better if the previous text fades-up and gives us that extra sense of dimension. We'll do that by targeting a previous class and fading it up transform: translateY(-20px);
. You can always target these with the nth
selector or in javascript, but to keep things simple I prefer to use classes.
&--default {
background-color: CORNFLOWERBLUE;
.button__text--default {
opacity: 1;
transform: translateY(0);
}
.button__text--success {
transform: translateY(-20px);
}
}
&--process {
background-color: MEDIUMSLATEBLUE;
.button__text--process {
opacity: 1;
transform: translateY(0);
}
.button__text--default {
transform: translateY(-20px);
}
}
&--success {
background-color: DARKTURQUOISE;
.button__text--success {
opacity: 1;
transform: translateY(0);
}
.button__text--process {
transform: translateY(-20px);
}
}
At this stage, you should already have a nice interaction going
To finalize our kickass send button, let's add a morphing icon. We'll use the CSS only approach, but you can always utilize SVGs as well.
First, we'll style our .button__icon-wrapper
and the initial .button__icon
(nested inside our .button
)
&__icon-wrapper {
width: 1.6em;
height: 1.6em;
margin-right: 1em;
display: inline-block;
vertical-align: top;
}
&__icon {
transition: all 300ms ease-in-out;
display: block;
box-sizing: border-box;
}
Once the base is ready, let's go ahead and style every icon. First is going to be a simple caret, we'll add it inside &--default
class. We'll simply create a square element, add top and right borders and then rotate it.
.button__icon {
width: 1em;
height: 1em;
margin: 0.3em 0.5em 0.3em 0;
border-right: 3px solid white;
border-top: 3px solid white;
transform: rotate(45deg);
}
For the &--process
, things get just a little more tricky, as we need to create a simple loading icon. We'll do that by utilizing clip-path
. You can read more about it here. We'll then animate the whole element with keyframes, rotating it and creating a spinner illusion.
.button__icon {
width: 1.2em;
height: 1.2em;
margin: 0.2em 0.5em 0.2em 0;
border-radius: 100px;
border: 3px solid white;
animation: loading 1s infinite ease;
clip-path: polygon(100% 0, 50% 50%, 100% 100%);
}
Keyframes:
@keyframes loading {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
Finally, let's create the success icon, a simple checkmark, similar to how we created the angle icon. Except let's rotate it an additional 360deg
to make the transition back to default more smooth, and apply reveal animation that uses clip-path
, essentially it reveals the bottom border first, and then moves to a 100% height.
You can play with clip-path
and adjust it with clippy. Once we apply rotation, it will reveal the icon following the checkmark line.
Without it, a smooth transformation from the previous icon is harder to achieve.
Inside &--success
let's add:
.button__icon {
margin: 0 0.3em 0.3em 0.1em;
height: 1.2em;
width: 0.9em;
border-radius: 0;
border-right: 3px solid white;
border-bottom: 3px solid white;
transform: rotate(405deg);
animation: reveal 500ms ease-in;
}
Keyframes (we'll also add a slight opacity transition):
@keyframes reveal {
0% {
opacity: 0;
clip-path: polygon(0 85%, 0 85%, 0 100%, 0 100%);
}
40% {
opacity: 0;
}
50% {
opacity: 1;
clip-path: polygon(0 85%, 100% 85%, 100% 100%, 0 100%);
}
100% {
clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
}
}
And our multi-state button is ready!
Of course, when you use it in production you should really fool-proof the way the classes are being toggled and handled! Are there any other micro-interactions you'd like to take a look at? If yes, let me know and my next post might be just about that!
Posted on February 8, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.