Sass mixin for CSS-only ripple effect

See the Pen by Teo Dragovic (@teodragovic) on CodePen.

Ripple effect is made popular by Material Design. Original implementation involves JavaScript but if you don’t care about ripple appearing exactly below the pointer, it can be done with pure CSS.

For my purposes, I combined and adapted solutions from Bence Szabo and Mladen Plavsic.

But my implementation had several issues.

First, it used transform hack to create stacking context. This made it difficult to add any additional animations on the element like exit/entrance etc.

I also didn’t wanna set z-index since ripple would be placed on all my buttons which can appear in various contexts. Having z-index set on an element preemptively could potentially break my layout in weird ways.

The solution was to use perspective property which would also create stacking context.

- transform: translate3d(0, 0, 0);
+ perspective: 1px;

Second, I had an issue with buttons disappearing in an older version of Chrome on Android. That that didn’t happen with ripple disabled. The issue was caused by using transform: scale to produce ripple animation (which I thought was clever due to performance benefits). Changing it to animate background-size instead, fixed it.

    &:before {
- transform: scale(12);
+ background-size: 15000%;

&:active:before {
- transform: scale(0, 0);
+ background-size: 1%;

Finally, buttons weren’t responding on the first click and they would cause layout shifts on iOS. These posts on StackOverflow nudged me to switch opacity values from 1 and 0 to floating-point values. That resolved rest of my bugs.

    &:before {
- opacity: 0;
+ opacity: 0.01;

&:active:before {
- opacity: 1;
+ opacity: 0.99;

Here is my final Sass mixin:

// Apply ripple effect on the element
// @param color of the ripple or falsey to remove previously set ripple
@mixin ripple($color) {
@if ($color) {
perspective: 1px;

&:before {
content: "";
display: block;
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
border-radius: inherit;
pointer-events: none;
background-image: radial-gradient(circle, $color 1%, transparent 1%);
background-repeat: no-repeat;
background-position: 50%;
background-size: 15000%;
opacity: 0.01;
transition: background 0.8s, opacity 1s;
z-index: -1;

&:active:before {
background-size: 1%;
opacity: 0.99;
transition: 0s !important;
@else {
@warn "Must pass color for ripple effect.";

You can use it on a clickable element (usually a button) by passing a color darker or lighter than the background.

button {
apperance: none;
border: none;
padding: 15px;
background: #eee;
@include ripple(#bbb);