Animating buttons with CSS and keyframes
Tags: html, css, javascript
Published: 2021-05-06 12:49:37

Let's say that you have a button in your HTML, and you want it to be animated when someone clicks it. I am going to show you here how you could do it with CSS Animations and a little bit of javascript.

## Initial setup
To begin with, I will be setting up some simple HTML and CSS with 3 neon styled buttons, so we can demonstrate some alternatives.
```html
<html>
    <head>
        <link rel="preconnect" href="https://fonts.gstatic.com">
        <link href="https://fonts.googleapis.com/css2?family=Ubuntu&display=swap" rel="stylesheet">
        <link rel="stylesheet" href="index.css">
        <script src="index.js"></script>
    </head>
    <body>
        <div class="container">
            <button class="btn btn-back" type="button">Back</button>
            <button class="btn btn-refresh" type="button">Refresh</button>
            <button class="btn btn-next" type="button">Next</button>
        </div>        
    </body>
</html>
```
```css
html, body {
    margin: 0;
    padding: 0;
}

body {
    background-color: black;
}

.container {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
}

.btn {
    min-width: 70px;
    max-width: 200px;
    margin: 1em;
    padding: 1em 2em;
    border-radius: 5px;
    border-width: 2px;
    background-color: black;
    font-family: 'Ubuntu', sans-serif;
    font-size: 1em;
    letter-spacing: 1px;
}

.btn:hover,
.btn:focus {
    cursor: pointer;
}

/* back button */
.btn-back {
    color: hotpink;
    border-color: hotpink;
}

/* refresh button */
.btn-refresh {
    color: orange;
    border-color: orange;
}

/* next button */
.btn-next {
    color: greenyellow;
    border-color: greenyellow;
}
```
This is how it looks like at Codepen. At the moment, buttons do nothing.
{% codepen https://codepen.io/eldocbrown/pen/LYWYWxZ %}

## Refresh Button
The first button I will be working with is the Refresh button. I am going to make the button's border wider when it is clicked, and then narrow it again by adding a class to the button element.
So in my CSS I'll add a keyframes at-rule with the animation steps and a class defining the animation style.
```css
@keyframes blinkingBorder {
    0% {border-width: 0.1em;}
    50% {border-width: 1em;}
    100% {border-width: 0.1em;}
}

.blink {
    animation-name: blinkingBorder;
    animation-duration: 0.1s;
    animation-iteration-count: 1;
}
```
What I am declaring with the blinkingBorder keyframes at-rule is that the `border-width` property should start and finish at `0.1em`, and to grow to `1em` in the middle of the animation.
Elements with the `blink` class should render the `blinkingBorder` animation for `0.1` seconds only `1` time.

To make this come to life, we have to create a click event handler for the refresh button and add (and the remove) the `blink` class to it.
```javascript
// When the HTML has finished loading...
document.addEventListener('DOMContentLoaded', () => {
    // Handle click event on the refresh button
    document.querySelector('.btn-refresh').addEventListener('click', e => handleRefreshClick(e))
})

const handleRefreshClick = (event) => {
    const className = 'blink'
    
    // Animate the clicked button (event.target)
    // by adding the blink class for 100 milliseconds
    animateButton(event.target, className, 100)
}

const animateButton = (button, classNameAnimation, milliseconds) => {

    // Remove the class if it exists
    button.classList.remove(classNameAnimation)
    
    // Add the class
    button.classList.add(classNameAnimation)

    // When the animation finishes, remove the class
    setTimeout(() => {
        button.classList.remove(classNameAnimation)
    }, milliseconds)
}
```
I wrote the class adding logic into the `animateButton` function so I can reuse it later with the other buttons. I'll   be adding a little more code to it later, though.

So let's see how this animation turned out on Codepen. Click on the Refresh button to test it.
{% codepen https://codepen.io/eldocbrown/pen/KKWKWeL %}

## Back Button
The second button I'll be addressing is the Back button. What I want here is that when the button is clicked, I get like a courtain effect opening to the left. To achieve this behavior, I'll first add some background CSS properties to the `btn-back` class, and use the `linear-gradient` CSS function.
```css
.btn-back {
    color: hotpink;
    border-color: hotpink;
    background: linear-gradient(90deg, hotpink 0 50%, transparent 50% 100%);
    background-size: 200%;
    background-position: 100%;   
}
```
What I'm declaring here is that the half of the button's background should be hotpink, instead of transparent (`background: linear-gradient(90deg, hotpink 0 50%, transparent 50% 100%);`), that it should be 2 times wider than the button (`background-size: 200%;`), and that it should be positioned at the top-right button's corner (`background-position: 100%;`)

Next, I'll set the CSS Animation at-rule and class.
```css
@keyframes fillOutFrames {
    0% {
        color: black;
        background-position: 0%;
    }
    100% {
        color: hotpink;
        background-position: 100%;
    }
}

.fillOut {
    animation-name: fillOutFrames;
    animation-duration: 0.5s;
    animation-iteration-count: 1;
}
```
This CSS is setting the animation to start with a black font color and a background position at the top-left corner, and end with a hotpink font color and a background position at the top-right corner. It last half a second and it runs once.
The trick here is to slide the button's background to the left, which is half hotpink and half transparent, giving us the visual effect that it is filling out that hotpink color from the button.

Lastly, I'll set up the click button's handler function in Javascript, which is pretty similar to the Refresh button's code. You'll see that the `animateButton` function is reused.
```javascript
// When the HTML has finished loading...
document.addEventListener('DOMContentLoaded', () => {
    // Handle click event on the refresh button
    document.querySelector('.btn-refresh').addEventListener('click', e => handleRefreshClick(e))
    // Handle click event on the back button
    document.querySelector('.btn-back').addEventListener('click', e => handleBackClick(e))
})

const handleBackClick = (event) => {
    const className = 'fillOut'
    
    // Animate the clicked button (event.target)
    // by adding the fillOut class for 500 milliseconds
    animateButton(event.target, className, 500)
}
```
So let's see you is it the aniamtion rendering on Codepen. Check the Back button out.
{% codepen https://codepen.io/eldocbrown/pen/bGqGjyY %}

## Next Button
This one will be the same as the Back button, except that I'll change the color, and that the background will slide from left to right, and the button will stay filled at the end of the animation. This will render a visual effect of the button filling in with a greenyellow color, from left to right.
To achieve the "*stay filled at the end*" part, what I'll do is to add a new `btn-next-final` class to the button when the animation finishes.
So the CSS will look like this.
```css
.btn-next {
    color: greenyellow;
    border-color: greenyellow;
    background: linear-gradient(90deg, greenyellow 0 50%, transparent 50% 100%);
    background-size: 200%;
    background-position: 100%;
}

.btn-next-final {
    color: black;
    background-position: 0%;
}

@keyframes fillInFrames {
    0% {
        color: greenyellow;
        background-position: 100%;
    }
    100% {
        color: black;
        background-position: 0%;
    }
}

.fillIn {
    animation-name: fillInFrames;
    animation-duration: 0.5s;
    animation-iteration-count: 1;
}
```
The new javscript is very similar, but I will be adding a parameter to the `animateButton` function so it takes a new `classNameFinal` parameter, with an `undefined` defualt value. This will be the class that I will add to the button at the end of the animation.
```javascript
// When the HTML has finished loading...
document.addEventListener('DOMContentLoaded', () => {
    // Handle click event on the refresh button
    document.querySelector('.btn-refresh').addEventListener('click', e => handleRefreshClick(e))
    // Handle click event on the back button
    document.querySelector('.btn-back').addEventListener('click', e => handleBackClick(e))
    // Handle click event on the next button
    document.querySelector('.btn-next').addEventListener('click', e => handleNextClick(e))
})

const handleNextClick = (event) => {
    const className = 'fillIn'
    const classNameFinal = 'btn-next-final'
    
    // Animate the clicked button (event.target)
    // by adding the fillIn class for 500 milliseconds
    // and adding the btn-next-final class at the end of the animation
    animateButton(event.target, className, 500, classNameFinal)
}

const animateButton = (button, classNameAnimation, milliseconds, classNameFinal = undefined) => {

    // Remove the class if it exists
    button.classList.remove(classNameAnimation)
    
    // Add the class
    button.classList.add(classNameAnimation)

    // When the animation finishes, remove the class
    // and add the final class, if provided
    setTimeout(() => {
        button.classList.remove(classNameAnimation)
        if (classNameFinal !== undefined) button.classList.add(classNameFinal)
    }, milliseconds)
}
```
Ok, let's see how is this button behaving on Codepen. Click the Next button to see the animation.
{% codepen https://codepen.io/eldocbrown/pen/jOBOvqv %}

Ok! I'm sure there are many more better ways to animate this buttons, so if you are in the mood to comment, please do so!

Thank you for reading!