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-ripple | Enables 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:
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:
<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>Position
Set where the ripple originates. The default is center.
Use arbitrary values to set a precise origin point with ripple-position-[<x>_<y>]:
Duration
Control how long the animation runs. The value multiplies 10ms, so ripple-duration-30 is 300ms. The default is 0.3s.
Fade
Fade the ripple out as it expands. Off by default.
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:
<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:
// 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}%`);
});// 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}%`);
});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>
);
}<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:
@import 'tw-jib-css/ripple';Baseline status
What these utilities are built on, live from the Web Platform Dashboard.