Easy micro-interactions in CSS (Pt2): Multi-state button

saintasia

Anastasia Kas

Posted on February 8, 2020

Easy micro-interactions in CSS (Pt2): Multi-state button

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

send button interactions

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.

  1. Adding an icon element
  2. Adding 3 text values
  3. Providing 3 styles for the button background-color
  4. 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!
Enter fullscreen mode Exit fullscreen mode

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;   
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
})

Enter fullscreen mode Exit fullscreen mode

At this point, you should have a button with vertically stacked text, which onClick will start changing colors (and classes):

button with stacked text

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;
  }
Enter fullscreen mode Exit fullscreen mode

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);
    }
  }

Enter fullscreen mode Exit fullscreen mode

At this stage, you should already have a nice interaction going

button before icons

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;
  }

Enter fullscreen mode Exit fullscreen mode

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);
    }

Enter fullscreen mode Exit fullscreen mode

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%);
    }

Enter fullscreen mode Exit fullscreen mode

Keyframes:

@keyframes loading {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
Enter fullscreen mode Exit fullscreen mode

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%);
  }
}
Enter fullscreen mode Exit fullscreen mode

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!

πŸ’– πŸ’ͺ πŸ™… 🚩
saintasia
Anastasia Kas

Posted on February 8, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related