Back
Mastering the Ripple Effect: A Guide to Building Engaging UI Buttons
Explore the art of creating an interactive button with a captivating ripple effect to enhance your web interface.
Introduction
Creating buttons that not only function well but also captivate users with engaging visuals can dramatically enhance user engagement on your website. In this tutorial, we'll build a button with a stunning ripple effect using pure HTML, CSS, and JavaScript.HTML Structure
Let's start with structuring the HTML. We'll need a container to center our button, and then we'll declare the button itself. The button will trigger the ripple effect upon click.
<div class="button-container">
<button class="ripple-button" onclick="createRipple(event)">Click Me</button>
</div>
CSS Styling
Our button is styled using CSS to give it a pleasant appearance, such as rounded corners and a color scheme. The ripple effect leverages CSS animations to create a visually appealing interaction. Here we define styles for the container to center the content using flexbox. The button itself is styled with colors and a hover effect:
.button-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.ripple-button {
position: relative;
overflow: hidden;
border: none;
padding: 15px 30px;
font-size: 16px;
color: #ffffff;
background-color: #6200ea;
cursor: pointer;
border-radius: 5px;
transition: background-color 0.3s;
}
.ripple-button:hover {
background-color: #3700b3;
}
The ripple class styles the span that we'll dynamically add to our button on click. Notice how it scales up and fades out, achieving the ripple effect:
.ripple {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.6);
transform: scale(0);
animation: ripple-animation 0.6s linear;
pointer-events: none;
}
@keyframes ripple-animation {
to {
transform: scale(4);
opacity: 0;
}
}
JavaScript Interaction
JavaScript adds a span to the button on every click and places its centre on the point that was clicked. The click position comes from event.clientX and event.clientY, which are measured from the top-left of the viewport, so the button position has to come from the same place: getBoundingClientRect().
function createRipple(event) {
const button = event.currentTarget;
const rect = button.getBoundingClientRect();
const scale = rect.width / button.offsetWidth || 1;
const diameter = Math.max(button.clientWidth, button.clientHeight);
const radius = diameter / 2;
// Keyboard clicks (Enter / Space) have detail 0 and no pointer position: start in the centre.
const x = event.detail === 0 ? button.clientWidth / 2 : (event.clientX - rect.left) / scale - button.clientLeft;
const y = event.detail === 0 ? button.clientHeight / 2 : (event.clientY - rect.top) / scale - button.clientTop;
const circle = document.createElement('span');
circle.style.width = circle.style.height = `${diameter}px`;
circle.style.left = `${x - radius}px`;
circle.style.top = `${y - radius}px`;
circle.classList.add('ripple');
circle.addEventListener('animationend', () => circle.remove());
const ripple = button.getElementsByClassName('ripple')[0];
if (ripple) {
ripple.remove();
}
button.appendChild(circle);
}
The diameter is the larger side of the button, and the keyframe scales the circle to 4x, so the ripple covers the whole button from any starting point. overflow: hidden on the button clips it to the rounded corners.Why getBoundingClientRect() and not offsetLeft
Many ripple snippets, including the first version of this one, subtract button.offsetLeft and button.offsetTop from the click position. That only works by accident. offsetLeft is measured from the button's offset parent (the nearest positioned ancestor, or the body), while clientX is measured from the viewport. As soon as the page is scrolled, the button sits inside a card with position: relative, or an ancestor is transformed, the two numbers no longer share an origin and the ripple starts somewhere else, often outside the button where overflow: hidden hides it. We measured it in Chrome: clicking a quarter of the way across the button started the old ripple about 280px away on a scrolled page with a positioned card, and near the opposite edge in the scaled preview above. With getBoundingClientRect() the ripple started within 1px of the pointer in every case. Two details make the maths exact. getBoundingClientRect() returns the size on screen, so if the button or a parent is scaled with transform, dividing by rect.width / button.offsetWidth converts back to the button's own pixels. Subtracting button.clientLeft and clientTop accounts for a border, because an absolutely positioned child is placed from the padding edge.Keyboard clicks, cleanup and reduced motion
Keyboard: pressing Enter or Space on a focused button fires a click with event.detail set to 0 and no pointer position (clientX and clientY are 0). Without a check the ripple would start in the top-left corner of the page. The function starts it from the centre of the button instead. Cleanup: the animationend listener removes each span as soon as its animation finishes, so finished ripples do not stay in the DOM. pointer-events: none on .ripple keeps the span from catching clicks or hover meant for the button. Reduced motion: for visitors who set prefers-reduced-motion, the ripple does not grow. It covers the button at once and fades out in 0.3s, which still confirms the click. animationend still fires, so cleanup works the same way.
@media (prefers-reduced-motion: reduce) {
.ripple {
transform: scale(4);
animation: ripple-fade 0.3s linear;
}
}
@keyframes ripple-fade {
to {
opacity: 0;
}
}
Many buttons: instead of an inline onclick on each button, attach the same function once:
document.querySelectorAll('.ripple-button').forEach((button) => {
button.addEventListener('click', createRipple);
});
Use pointerdown instead of click if you want the ripple to start the moment the finger or mouse goes down, as in Material Design. Keep the click listener as well if the button must show the ripple for keyboard users, since pointerdown does not fire for Enter or Space.Advanced Customizations
To take this component further, you might consider adjusting the colors and timing of the ripple effect for different themes or accessibility purposes. You can also incorporate more complex animations or integrate the ripple effect into larger, interactive components for a more cohesive user experience across your application. The styles can also be adjusted for mobile support by adjusting button dimensions and ensuring the ripple calculations consider dynamic screen sizes and touch events. This flexibility allows the button to be a great tool for responsive web design.When to Use This
Ripple effect buttons are excellent for scenarios where visual feedback is necessary to make interactions delightfully clear and engaging. Implement them in situations where user input is crucial, such as form submissions, navigational controls, or any dynamic interaction that benefits from visual acknowledgment. The ripple effect adds a level of sophistication and polish, making your user interface not only functional but also aesthetically pleasing and enjoyable to use. We've equipped you with the means to create an interactive experience that enhances user involvement. Feel free to modify and extend these principles to tailor your innovative designs.Whole code
<div class="button-container">
<button class="ripple-button" onclick="createRipple(event)">Click Me</button>
</div>
<script>
function createRipple(event) {
const button = event.currentTarget;
const rect = button.getBoundingClientRect();
const scale = rect.width / button.offsetWidth || 1;
const diameter = Math.max(button.clientWidth, button.clientHeight);
const radius = diameter / 2;
// Keyboard clicks (Enter / Space) have detail 0 and no pointer position: start in the centre.
const x = event.detail === 0 ? button.clientWidth / 2 : (event.clientX - rect.left) / scale - button.clientLeft;
const y = event.detail === 0 ? button.clientHeight / 2 : (event.clientY - rect.top) / scale - button.clientTop;
const circle = document.createElement('span');
circle.style.width = circle.style.height = `${diameter}px`;
circle.style.left = `${x - radius}px`;
circle.style.top = `${y - radius}px`;
circle.classList.add('ripple');
circle.addEventListener('animationend', () => circle.remove());
const ripple = button.getElementsByClassName('ripple')[0];
if (ripple) {
ripple.remove();
}
button.appendChild(circle);
}
</script>
<style>
.button-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.ripple-button {
position: relative;
overflow: hidden;
border: none;
padding: 15px 30px;
font-size: 16px;
color: #ffffff;
background-color: #6200ea;
cursor: pointer;
border-radius: 5px;
transition: background-color 0.3s;
}
.ripple-button:hover {
background-color: #3700b3;
}
.ripple {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.6);
transform: scale(0);
animation: ripple-animation 0.6s linear;
pointer-events: none;
}
@keyframes ripple-animation {
to {
transform: scale(4);
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.ripple {
transform: scale(4);
animation: ripple-fade 0.3s linear;
}
}
@keyframes ripple-fade {
to {
opacity: 0;
}
}
</style>
Thank you for reading this article.
Comments