Skip to content

Ripple ​

Material-style ripple effect on click. Pure CSS, no JavaScript. The ripple always runs its full duration, even if you let go early.

Browser support

Two features set the floor: @property, which carries the animated transitions, and color-mix(), which builds the ripple color itself. @property landed first in every engine, but without color-mix() the ripple color is invalid and nothing paints, so color-mix() is the one that matters. No relative color syntax is involved. Live status for both is at the foot of this page.

Quick reference ​

class styles
bg-rippleEnables ripple effect on :active via radial-gradient animation
ripple-color-<color>--jib-ripple-color: <color>
ripple-color-<color>/<opacity>--jib-ripple-color: <color> at <opacity>%
ripple-color-<color>/[<percentage>]--jib-ripple-color: <color> at that percentage
ripple-color-<color>/[<number>]--jib-ripple-color: <color> at <number> × 100%
ripple-color-current--jib-ripple-color: currentColor
ripple-color-[<value>]--jib-ripple-color: <value>
ripple-color-(--var)--jib-ripple-color: var(--var)
ripple-duration-<number>--jib-ripple-duration: calc(<number> * 10ms)
ripple-duration-[<value>]--jib-ripple-duration: <value>
ripple-position-center--jib-ripple-position: center
ripple-position-top--jib-ripple-position: top

Basic usage ​

Add bg-ripple to any element to enable the ripple effect on click:

Ripple color ​

The default ripple is white at 20% opacity. Use ripple-color-<color> to customise:

white
indigo-300
amber-300
current

Use ripple-color-current to match the ripple to the element's text color. Handy on outlined or ghost buttons, where the text color is the theme.

Opacity ​

Control ripple opacity with the slash modifier. It takes the same three spellings Tailwind's own opacity modifiers do — a bare number is a percentage, a bracketed percentage passes through, and a bracketed number is a 0–1 alpha:

html
<div class="bg-ripple ripple-color-red-500/50"></div>
<div class="bg-ripple ripple-color-red-500/[50%]"></div>
<div class="bg-ripple ripple-color-red-500/[0.5]"></div>
/90
/70
/50
/30
/10

Position ​

Set where the ripple originates. The default is center.

center
top
bottom
left
right

Use arbitrary values to set a precise origin point with ripple-position-[<x>_<y>]:

12px 8px
73% 15%
4px 85%

Duration ​

Control how long the animation runs. The value multiplies 10ms, so ripple-duration-30 is 300ms. The default is 0.3s.

200ms
400ms
800ms
2s

Fade ​

Fade the ripple out as it expands. Off by default.

fade
fade-50
fade-80
fade-none

Using a custom value ​

Use the ripple-color-[<value>] syntax to pass a value the palette doesn't have:

Using a custom variable ​

Reference a CSS variable with (--var). Every ripple utility takes one kind of value, so none of them needs a type hint:

The same applies to position, duration and fade:

html
<div class="bg-ripple ripple-position-(--ripple-pos) [--ripple-pos:25%_75%]"></div>

A type hint is still accepted — ripple-color-(color:--var) works — and is needed on utilities whose namespace takes more than one kind of value, such as bg-* and the gradient stops. See Colors from CSS variables need a type hint.

Cursor-tracking ripple ​

By default, ripple-position is a fixed value, so the ripple always starts from the same point. To start it wherever the user clicks, bind ripple-position to a CSS variable and update it with JavaScript on each mousedown:

The JavaScript is small. Convert the cursor position to a percentage and write it to --ripple-pos on each mousedown:

js
// HTML: <button class="ripple-btn bg-ripple ripple-position-(--ripple-pos)">Click me</button>

const button = document.querySelector('.ripple-btn');

button.addEventListener('mousedown', (e) => {
  const rect = button.getBoundingClientRect();
  const x = (((e.clientX - rect.left) / rect.width) * 100).toFixed(1);
  const y = (((e.clientY - rect.top) / rect.height) * 100).toFixed(1);
  button.style.setProperty('--ripple-pos', `${x}% ${y}%`);
});
ts
// HTML: <button class="ripple-btn bg-ripple ripple-position-(--ripple-pos)">Click me</button>

const button = document.querySelector<HTMLButtonElement>('.ripple-btn')!;

button.addEventListener('mousedown', (e: MouseEvent) => {
  const rect = button.getBoundingClientRect();
  const x = (((e.clientX - rect.left) / rect.width) * 100).toFixed(1);
  const y = (((e.clientY - rect.top) / rect.height) * 100).toFixed(1);
  button.style.setProperty('--ripple-pos', `${x}% ${y}%`);
});
jsx
function RippleButton() {
  function handleMouseDown(e) {
    const rect = e.currentTarget.getBoundingClientRect();
    const x = (((e.clientX - rect.left) / rect.width) * 100).toFixed(1);
    const y = (((e.clientY - rect.top) / rect.height) * 100).toFixed(1);
    e.currentTarget.style.setProperty('--ripple-pos', `${x}% ${y}%`);
  }

  return (
    <button className="bg-ripple ripple-position-(--ripple-pos)" onMouseDown={handleMouseDown}>
      Click me
    </button>
  );
}
vue
<template>
  <button
    ref="buttonRef"
    class="bg-ripple ripple-position-(--ripple-pos)"
    @mousedown="handleMouseDown"
  >
    Click me
  </button>
</template>

<script setup>
import { useTemplateRef } from 'vue';

const buttonRef = useTemplateRef('buttonRef');

function handleMouseDown(e) {
  const rect = buttonRef.value.getBoundingClientRect();
  const x = (((e.clientX - rect.left) / rect.width) * 100).toFixed(1);
  const y = (((e.clientY - rect.top) / rect.height) * 100).toFixed(1);
  buttonRef.value.style.setProperty('--ripple-pos', `${x}% ${y}%`);
}
</script>

Why not just set --jib-ripple-position directly?

You could, but a custom variable via ripple-position-(--ripple-pos) keeps the contract explicit. Tailwind sees the utility in your markup and emits the ripple-position rule. Setting the internal variable directly works at runtime, but the utility won't appear in your compiled CSS unless something else references it.

Applying conditionally ​

Import ​

Included in @import 'tw-jib-css'. To import individually:

css
@import 'tw-jib-css/ripple';

Baseline status

What these utilities are built on, live from the Web Platform Dashboard.