How to Recreate the Ripple Effect of Material Design Buttons
Publikováno: 12.10.2020
When I first discovered Material Design, I was particularly inspired by its button component. It uses a ripple effect to give users feedback in a simple, elegant way.
How does this effect work? Material Design’s buttons don’t just sport a neat ripple animation, but the animation also changes position depending on where each button is clicked.
We can achieve the same result. We’ll start with a concise solution using ES6+ JavaScript, before looking at a few alternative approaches.
HTML… Read article “How to Recreate the Ripple Effect of Material Design Buttons”
The post How to Recreate the Ripple Effect of Material Design Buttons appeared first on CSS-Tricks.
You can support CSS-Tricks by being an MVP Supporter.
When I first discovered Material Design, I was particularly inspired by its button component. It uses a ripple effect to give users feedback in a simple, elegant way.
How does this effect work? Material Design’s buttons don’t just sport a neat ripple animation, but the animation also changes position depending on where each button is clicked.
We can achieve the same result. We’ll start with a concise solution using ES6+ JavaScript, before looking at a few alternative approaches.
HTML
Our goal is to avoid any extraneous HTML markup. So we’ll go with the bare minimum:
<button>Find out more</button>
Styling the button
We’ll need to style a few elements of our ripple dynamically, using JavaScript. But everything else can be done in CSS. For our buttons, it’s only necessary to include two properties.
button {
position: relative;
overflow: hidden;
}
Using position: relative
allows us to use position: absolute
on our ripple element, which we need to control its position. Meanwhile, overflow: hidden
prevents the ripple from exceeding the button’s edges. Everything else is optional. But right now, our button is looking a bit old school. Here’s a more modern starting point:
/* Roboto is Material's default font */
@import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap');
button {
position: relative;
overflow: hidden;
transition: background 400ms;
color: #fff;
background-color: #6200ee;
padding: 1rem 2rem;
font-family: 'Roboto', sans-serif;
font-size: 1.5rem;
outline: 0;
border: 0;
border-radius: 0.25rem;
box-shadow: 0 0 0.5rem rgba(0, 0, 0, 0.3);
cursor: pointer;
}
Styling the ripples
Later on, we’ll be using JavaScript to inject ripples into our HTML as spans with a .ripple
class. But before turning to JavaScript, let’s define a style for those ripples in CSS so we have them at the ready:
span.ripple {
position: absolute; /* The absolute position we mentioned earlier */
border-radius: 50%;
transform: scale(0);
animation: ripple 600ms linear;
background-color: rgba(255, 255, 255, 0.7);
}
To make our ripples circular, we’ve set the border-radius
to 50%. And to ensure each ripple emerges from nothing, we’ve set the the default scale to 0. Right now, we won’t be able to see anything because we don’t yet have a value for the top
, left
, width
, or height
properties; we’ll soon be injecting these properties with JavaScript.
As for our CSS, the last thing we need to add is an end state for the animation:
@keyframes ripple {
to {
transform: scale(4);
opacity: 0;
}
}
Notice that we’re not defining a starting state with the from
keyword in the keyframes? We can omit from
and CSS will construct the missing values based on those that apply to the animated element. This occurs if the relevant values are stated explicitly — as in transform: scale(0)
— or if they’re the default, like opacity: 1
.
Now for the JavaScript
Finally, we need JavaScript to dynamically set the position and size of our ripples. The size should be based on the size of the button, while the position should be based on both the position of the button and of the cursor.
We’ll start with an empty function that takes a click event as its argument:
function createRipple(event) {
//
}
We’ll access our button by finding the currentTarget
of the event.
const button = event.currentTarget;
Next, we’ll instantiate our span element, and calculate its diameter and radius based on the width and height of the button.
const circle = document.createElement("span");
const diameter = Math.max(button.clientWidth, button.clientHeight);
const radius = diameter / 2;
We can now define the remaining properties we need for our ripples: the left
, top
, width
and height
.
circle.style.width = circle.style.height = `${diameter}px`;
circle.style.left = `${event.clientX - (button.offsetLeft + radius)}px`;
circle.style.top = `${event.clientY - (button.offsetTop + radius)}px`;
circle.classList.add("ripple");
Before adding our span element to the DOM, it’s good practice to check for any existing ripples that might be leftover from previous clicks, and remove them before executing the next one.
const ripple = button.getElementsByClassName("ripple")[0];
if (ripple) {
ripple.remove();
}
As a final step, we append the span as a child to the button element so it is injected inside the button.
button.appendChild(circle);
With our function complete, all that’s left is to call it. This could be done in a number of ways. If we want to add the ripple to every button on our page, we can use something like this:
const buttons = document.getElementsByTagName("button");
for (const button of buttons) {
button.addEventListener("click", createRipple);
}
We now have a working ripple effect!
Taking it further
What if we want to go further and combine this effect with other changes to our button’s position or size? The ability to customize is, after all, one of the main advantages we have by choosing to recreate the effect ourselves. To test how easy it is to extend our function, I decided to add a “magnet” effect, which causes our button to move towards our cursor when the cursor’s within a certain area.
We need to rely on some of the same variables defined in the ripple function. Rather than repeating code unnecessarily, we should store them somewhere they’re accessible to both methods. But we should also keep the shared variables scoped to each individual button. One way to achieve this is by using classes, as in the example below:
Since the magnet effect needs to keep track of the cursor every time it moves, we no longer need to calculate the cursor position to create a ripple. Instead, we can rely on cursorX
and cursorY
.
Two important new variables are magneticPullX
and magneticPullY
. They control how strongly our magnet method pulls the button after the cursor. So, when we define the center of our ripple, we need to adjust for both the position of the new button (x
and y
) and the magnetic pull.
const offsetLeft = this.left + this.x * this.magneticPullX;
const offsetTop = this.top + this.y * this.magneticPullY;
To apply these combined effects to all our buttons, we need to instantiate a new instance of the class for each one:
const buttons = document.getElementsByTagName("button");
for (const button of buttons) {
new Button(button);
}
Other techniques
Of course, this is only one way to achieve a ripple effect. On CodePen, there are lots of examples that show different implementations. Below are some of my favourites.
CSS-only
If a user has disabled JavaScript, our ripple effect doesn’t have any fallbacks. But it’s possible to get close to the original effect with just CSS, using the :active pseudo-class to respond to clicks. The main limitation is that the ripple can only emerge from one spot — usually the center of the button — rather than responding to the position of our clicks. This example by Ben Szabo is particularly concise:
Pre-ES6 JavaScript
Leandro Parice’s demo is similar to our implementation but it’s compatible with earlier versions of JavaScript:
jQuery
This example use jQuery to achieve the ripple effect. If you already have jQuery as a dependency, it could help save you a few lines of code.
React
Finally, one last example from me. Although it’s possible to use React features like state and refs to help create the ripple effect, these aren’t strictly necessary. The position and size of the ripple both need to be calculated for every click, so there’s no advantage to holding that information in state. Plus, we can access our button element from the click event, so we don’t need refs either.
This React example uses a createRipple
function identical to that of this article’s first implementation. The main difference is that — as a method of the Button
component — our function is scoped to that component. Also, the onClick
event listener is now part of our JSX:
The post How to Recreate the Ripple Effect of Material Design Buttons appeared first on CSS-Tricks.
You can support CSS-Tricks by being an MVP Supporter.