Animating a Smooth Ripple Effect With JavaScript

- 4 minutes read

One thing I like about Material Design, 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 of what we’ll create, and I’ve uploaded the code to a public repository.

Link to this section 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:

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:

@-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:

@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:

.ripple {
    animation: .5s linear;
}

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

Link to this section 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.

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

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:

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:

// 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:

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:

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:

// `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.

Link to this section 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!