Animating a Smooth Ripple Effect With JavaScript
Tags: javascript, css, design, webdev
Published: 2021-08-27 06:14:55

One thing I like about [Material Design](https://material.io/), a library of UI components for the web, is the ripple animation that their buttons emit when clicked.

Let's create a similar but adaptable ripple effect using JavaScript (and a bit of CSS) that we can add to our own buttons!

I've published a [demo](https://ripple-animation.vercel.app/) of what we'll create, and I've uploaded [the code](https://github.com/natclark/ripple-animation) to a public repository.

## Styling the animation

First, let's get the CSS out of the way.

Each ripple will be a `<span>` element that we briefly attach to the page.

Let's style that span element. I'll use a class selector called `ripple`:

```css
span.ripple {
    background-color: rgba(255, 255, 255, .5);
    border-radius: 50%;
    height: 0;
    opacity: 1;
    position: absolute;
    transform: scale(0);
    width: 0;
    will-change: opacity, transform;
}
```

To create that rippling effect, we'll have to animate our `<span>` element's size and opacity.

So, let's adjust the `opacity` and `transform` properties within a keyframe animation:

```css
@-webkit-keyframes ripple {
    100% {
        opacity: 0;
        transform: scale(2);
    }
}
@-moz-keyframes ripple {
    100% {
        opacity: 0;
        transform: scale(2);
    }
}
@keyframes ripple {
    100% {
        opacity: 0;
        transform: scale(2);
    }
}
```

For accessibility purposes, I strongly encourage you to add the following snippet at the top or bottom of your file.

It will disable animations for users with browsers that are configured to request reduced motion:
```css
@media (prefers-reduced-motion: reduce) {
    * {
        -webkit-transition-property: none;
        -moz-transition-property: none;
        transition-property: none;
    }
}
```

To ensure browser compatibility, I've included `@-webkit-keyframes` and `@-moz-keyframes` selectors in addition to the standard `@keyframes` selector.

There's just one last bit of CSS that we'll have to implement, which will attach our new keyframe directly to our ripple's selector:

```css
.ripple {
    animation: .5s linear;
}
```

I've set the duration to half a seccond, but you can modify it as you see fit.

## Animating click events

It's now time to write some JavaScript and put our ripple animation to the test.

First, we'll create a ripple function, and attach it to an `onclick` [event listener](https://www.w3schools.com/js/js_htmldom_eventlistener.asp).

I'll attach mine to every `<button>` element in the document:

```js
const ripple = (e) => {
    // Our code will go here.
};
document.querySelectorAll(`button`).forEach((button) => {
    button.addEventListener(`click`, ripple);
});
```

It's very important to note that the `<button>` element we're setting our ripple event to **must have `position` set to `relative` and `overflow` set to `hidden`**.

Otherwise, our ripple, which has its `position` set to `absolute`, will not display properly.

I'll use the following CSS on my `<button>` elements:

```css
button {
    overflow: hidden;
    position: relative;
}
```

Alright, let's first grab the width and height of the clicked button, so that we know how big to make our ripple:

```js
// e.target represents the button that was clicked
let width = e.target.offsetWidth;
let height = e.target.offsetHeight;

width >= height ? (height = width) : (width = height);
```

The ternary condition on that last line sets the smaller side to the length of the bigger side.

For example, if the width of the button is 50px, and the height is 30px, the ripple will become 50px50px large.

This will make our ripple look smoother and more even.

Next, let's add that `<span>` element I was talking about earlier to the page, place it within the clicked button, and add some conditional styles to it:

```js
const ripple = document.createElement(`span`);
e.target.appendChild(ripple);

ripple.style = `
    height: ${height}px !important;
    left: ${e.pageX - e.target.offsetLeft - width / 2}px !important;
    top: ${e.pageY - e.target.offsetTop - height / 2}px !important;
    width: ${width}px !important;
`.trim(); // `trim()` will remove the trailing whitespace.
```

These conditional styles will resize our ripple and position it within the center of the button.

Now that our ripple is placed within the clicked button, properly resized, and positioned at the center, we can animate it:

```js
ripple.classList.add(`ripple`);
```

Remember that CSS selector we were styling earlier?

Well, it's now been added to our ripple element, which means that the keyframe we created and tied to that selector will trigger.

I set the duration to half a second, so I'll remove the ripple element after `500ms`:

```js
// `setTimeout()` uses milliseconds as the base unit:
setTimeout(() => e.target.removeChild(ripple), 500);
```

This way, the DOM doesn't get cluttered with countless hidden ripples.

## Conclusion

Well, we've created a flexible ripple animation that we can add to our buttons when they're clicked!

I really think I should be using this animation more, because it adds such a nice touch to just about any modern UI.

I hope you found this guide useful!