Ripple
Material-style ripple effect triggered on click — pure CSS, no JavaScript. Uses @property transitions to expand a radial gradient outward from the click point. The ripple always completes its full duration, even if the click is released early.
Import
Included in @import 'tw-jib-css'. To import individually:
@import 'tw-jib-css/ripple';Quick Reference
| Class | Styles |
|---|---|
| bg-ripple | Enables ripple effect on :active via radial-gradient animation |
| ripple-color-<color> | --ripple-color: <color> |
| ripple-color-<color>/<opacity> | --ripple-color: color-mix(in oklch, <color> calc(<opacity> * 1%), transparent) |
| ripple-color-current | --ripple-color: currentColor |
| ripple-color-[<value>] | --ripple-color: <value> |
| ripple-duration-<number> | --ripple-duration: calc(<number> * 10ms) |
| ripple-duration-[<value>] | --ripple-duration: <value> |
| ripple-position-center | --ripple-position: center |
| ripple-position-top | --ripple-position: top |
| ripple-position-bottom | --ripple-position: bottom |
| ripple-position-left | --ripple-position: left |
| ripple-position-right | --ripple-position: right |
Basic Usage
Add bg-ripple to any element to enable the ripple effect on click:
Ripple Colour
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 colour. This is useful for outlined or ghost buttons where the text colour defines the theme.
Opacity
Opacity is applied using color-mix(in oklch). See the Colour Spaces guide to learn why oklch is used as the default mixing space.
Control ripple opacity with the slash modifier:
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 ripple animation takes. The value is multiplied by 10ms, so ripple-duration-30 = 300ms. The default is 0.3s.
Fade
Enable the ripple to fade out as it expands. By default there is no fade.
Using a custom value
Use the ripple-color-[<value>] syntax to set ripple properties based on a completely custom value:
Using a custom variable
For CSS variables, use the typed bare-value syntax ripple-color-(color:--var). The color type hint tells Tailwind to interpret the variable as a colour:
The same pattern works for all ripple properties. For position, use the position type hint:
<div class="bg-ripple ripple-position-(position:--ripple-pos)" style="--ripple-pos: 25% 75%">Cursor-tracking ripple
By default, ripple-position is a fixed value — the ripple always starts from the same point. To make the ripple originate from where the user actually clicks, bind ripple-position to a CSS variable and update it with JavaScript on each mousedown:
The JavaScript is minimal — 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-(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-(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-(position:--ripple-pos)"
onMouseDown={handleMouseDown}
>
Click me
</button>
);
}<template>
<button
ref="buttonRef"
class="bg-ripple ripple-position-(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 --tw-jib--ripple-position directly?
You could — but using a custom variable via ripple-position-(position:--ripple-pos) keeps the contract explicit. Tailwind sees the utility in your markup and includes the ripple-position rule in the output. Setting the internal variable directly works at runtime, but the utility won't appear in your compiled CSS unless something else references it.