Skip to content

Home

Button with ripple effect

Ripple effects on click were popularized by the Material Design guidelines. They provide visual feedback to the user when interacting with a button. While seemingly simple, they're a little tricky to implement overall.

Anatomy of the button

The button consists of two main parts: the ripple circle and the button content. In this implementation, the ripple circle is a span element with the ripple class. The button content is a span element with the content class. The ripple circle is positioned absolutely inside the button and is animated when the button is clicked.

<button class="ripple-button">
  <span class="ripple"></span>
  <span class="content">Click me</span>
</button>

The ripple animation

For the ripple effect animation, we'll use the transform property to scale the ripple circle and the opacity property to fade it out. The animation property is used to define the animation duration and easing function. We'll also have to use a value of forwards for the animation-fill-mode property to keep the last keyframe state after the animation ends.

@keyframes ripple-effect {
  0% {
    transform: scale(1);
    opacity: 1;
  }
  50% {
    transform: scale(10);
    opacity: 0.375;
  }
  100% {
    transform: scale(35);
    opacity: 0;
  }
}

Positioning the ripple circle

The ripple circle is positioned absolutely inside the button. The left and top properties are set to the pointer's coordinates when the button is clicked. This way, the ripple circle appears to originate from the pointer's position.

In order for this to work in React, we'll need to use the useState() and useEffect() hooks to manage the button's state and the animation. The state will hold the coordinates and the animation state of the button. Then, using the useEffect() hook, we'll update the state variables and start the animation when the button is clicked.

A setTimeout() call is used to clear the animation after it's done playing. Another useEffect() hook is used to reset the coordinates whenever the animation is not playing. Finally, the onClick event is handled by updating the coordinates and calling the passed callback.

Putting it all together, here's the complete implementation of a ripple effect button:

.ripple-button {
  border-radius: 4px;
  border: none;
  margin: 8px;
  padding: 14px 24px;
  background: #1976d2;
  color: #fff;
  overflow: hidden;
  position: relative;
  cursor: pointer;
}

.ripple-button > .ripple {
  width: 20px;
  height: 20px;
  position: absolute;
  background: #63a4ff;
  display: block;
  content: "";
  border-radius: 9999px;
  opacity: 1;
  animation: 0.9s ease 1 forwards ripple-effect;
}

@keyframes ripple-effect {
  0% {
    transform: scale(1);
    opacity: 1;
  }
  50% {
    transform: scale(10);
    opacity: 0.375;
  }
  100% {
    transform: scale(35);
    opacity: 0;
  }
}

.ripple-button > .content {
  position: relative;
  z-index: 2;
}
const RippleButton = ({ children, onClick }) => {
  const [coords, setCoords] = React.useState({ x: -1, y: -1 });
  const [isRippling, setIsRippling] = React.useState(false);

  React.useEffect(() => {
    if (coords.x !== -1 && coords.y !== -1) {
      setIsRippling(true);
      setTimeout(() => setIsRippling(false), 300);
    } else setIsRippling(false);
  }, [coords]);

  React.useEffect(() => {
    if (!isRippling) setCoords({ x: -1, y: -1 });
  }, [isRippling]);

  return (
    <button
      className="ripple-button"
      onClick={e => {
        const rect = e.target.getBoundingClientRect();
        setCoords({ x: e.clientX - rect.left, y: e.clientY - rect.top });
        onClick && onClick(e);
      }}
    >
      {isRippling ? (
        <span
          className="ripple"
          style={{
            left: coords.x,
            top: coords.y
          }}
        />
      ) : (
        ''
      )}
      <span className="content">{children}</span>
    </button>
  );
};

ReactDOM.createRoot(document.getElementById('root')).render(
  <RippleButton onClick={e => console.log(e)}>Click me</RippleButton>
);

More like this

Start typing a keyphrase to see matching snippets.