Easy micro-interactions in CSS (Pt2): Multi-state button
Tags: css, microinteractions, ui, frontend
Published: 2020-02-08 08:02:44

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 - <a href="https://dev.to/saintasia/easy-micro-interactions-in-css-pt1-hamburger-menu-icons-541j">hamburger icon</a>.

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](https://dev-to-uploads.s3.amazonaws.com/i/war91vp8mh0xkjeogn8h.gif)

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:

```slim
  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 <a target="_blank" href="https://htmlcolorcodes.com/color-names/">here</a>):
```scss
.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()`.

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


![button with stacked text](https://dev-to-uploads.s3.amazonaws.com/i/5g7cgzc703uhtxe18v6o.png)

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

```scss
  &__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.

```scss
  &--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

![button before icons](https://dev-to-uploads.s3.amazonaws.com/i/sqsh1czbb67iisa6askt.gif)

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

```scss
  &__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.

```scss
   .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  <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/clip-path" target="_blank">here</a>. We'll then animate the whole element with keyframes, rotating it and creating a spinner illusion.

```scss
    .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:
```scss
@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 <a href="https://bennettfeely.com/clippy/">clippy</a>. 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:

```scss
    .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):
```scss
@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!

{% codepen https://codepen.io/saintasia/pen/JjdoPXy %}


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!