CSS Ripple Effect — Tutorial & Generator

Learn how to create a ripple effect with radial gradients, keyframes, and SVG filters, then see it as a fullscreen scroll transition.

Create a Ripple Effect with CSS

Hover over the preview to trigger the ripple. Adjust speed, number of rings, and color below, then copy the generated CSS.

How the hover ripple works ↓
0.3s2.5s
1.5x6x
0px30px
110

Ripple Hover Effect

Expanding ring animation on hover with configurable style, speed, blur, and scale

What Is the Ripple Effect?

A ripple effect is an animation that mimics the concentric circles created when a drop hits a water surface. In web design, it's used as visual feedback on buttons and interactive elements (popularised by Material Design), as hover and click animations on cards and links, as background or hero-section decoration on water-themed and wellness sites, and as fullscreen scroll transitions between sections.

See This Effect as a Scroll Transition

The CSS version above works for hover and click interactions. But imagine this ripple effect playing between fullscreen sections as users scroll through your website, with sine-wave displacement computed per-pixel on the GPU, creating realistic fluid distortion at 60 fps.

Scroll inside the preview below and adjust the speed to see how it feels at different paces:

Loading preview...

Included out of the box

  • Touch & swipe support
  • Scroll snapping
  • Keyboard navigation
  • 80+ WebGL effects
  • 60 fps GPU-accelerated
  • One-liner configuration
View Pricing →

CSS Background Ripple Effect with SVG Filters

The water ripple applies an SVG <filter> to an image element using the CSS filter: url(#id) property. Inside the filter, <feTurbulence> generates Perlin noise, a mathematically smooth random pattern, and <feDisplacementMap> uses that noise to shift pixels in the source image. The result is an organic, fluid-like distortion that looks like light refracting through a water surface.

<svg style="position:absolute; width:0; height:0">
  <filter id="water-ripple">
    <feTurbulence baseFrequency="0.01 0.05" numOctaves="2" result="turb">
      <animate attributeName="baseFrequency" dur="2s"
        values="0.01 0.05;0.03 0.08;0.01 0.05"
        repeatCount="indefinite"/>
    </feTurbulence>
    <feDisplacementMap in="SourceGraphic" in2="turb"
      scale="15" xChannelSelector="R" yChannelSelector="G"/>
  </filter>
</svg>

<!-- Apply with CSS -->
<img style="filter: url(#water-ripple)" src="photo.jpg">

The animation is handled by the <animate> element that oscillates baseFrequency over time. As the frequency changes, the noise pattern shifts, creating continuous undulating movement. The scale attribute on feDisplacementMap controls how aggressively pixels are displaced; low values (5–15) produce a subtle heat-haze; high values (30+) give a heavy underwater distortion.

This is a pure CSS background ripple effect: no JavaScript, no canvas, and it works on any HTML element. SVG filters are supported in 97%+ of browsers, though the filter animation is not GPU-composited in all of them, so performance can vary on complex pages. For fullscreen or scroll-driven ripples, WebGL is the better choice.

WebGL Image Ripple Effect

The WebGL ripple runs a fragment shader on the GPU that computes per-pixel displacement using a sine-wave function. The shader takes the current UV coordinate, calculates the distance from a ripple origin point, and offsets the texture lookup based on that distance:

// Fragment shader — core displacement
float dist = distance(uv, rippleOrigin);
vec2 offset = normalize(uv - rippleOrigin)
  * sin(dist * frequency - time * speed)
  * strength;
gl_FragColor = texture2D(image, uv + offset);

Three uniforms control the effect: strength (how far pixels are displaced), frequency (how many wave peaks fit across the image), and speed (how fast the waves propagate). The animation loop uses requestAnimationFrame and updates a single time uniform each frame, so the GPU handles all the heavy computation with no DOM manipulation and no reflow.

Unlike the SVG filter approach, WebGL ripples are fully GPU-composited and scale to any resolution without performance degradation. Images must be served from the same origin or a CORS-enabled CDN, since the shader reads pixel data via texImage2D. This is the approach used by fullPage.js for its fullscreen scroll ripple transitions.

Pure CSS Ripple Effect on Click

The pure CSS approach to a ripple effect on click uses the background shorthand with a radial-gradient and background-size toggling. In the resting state, the button has a solid background-color. On :hover, an oversized radial gradient creates a tiny transparent dot at the centre surrounded by the hover colour:

.ripple-btn {
  background-position: center;
  transition: background 0.8s;
}
.ripple-btn:hover {
  background: #47a7f5
    radial-gradient(circle, transparent 1%, #47a7f5 1%)
    center / 15000%;
}
.ripple-btn:active {
  background-size: 100%;
  transition: background 0s;
}

On :active, background-size snaps to 100% with transition: background 0s, causing the transparent dot to instantly fill the button. When the user releases and the :active state ends, the background-size transitions back to 15000% with an 0.8s ease. This expanding transition is the ripple. The result closely matches the Material UI ripple effect CSS pattern without any JavaScript, making it ideal for lightweight sites.

The limitation is that the ripple always originates from the centre of the button, not from the exact click position. For pointer-accurate ripples (like the real Material Design component), you need JavaScript to set the origin. See the JS Button tab.

JavaScript Button Ripple Effect

The JavaScript approach creates a ripple element dynamically at the exact click position. On each click event, a <span> with border-radius: 50% is created and positioned at the pointer coordinates:

button.addEventListener('click', function(e) {
  const circle = document.createElement('span');
  const d = Math.max(this.clientWidth, this.clientHeight);
  circle.style.width = circle.style.height = d + 'px';
  circle.style.left = e.clientX - this.offsetLeft - d/2 + 'px';
  circle.style.top  = e.clientY - this.offsetTop  - d/2 + 'px';
  circle.classList.add('ripple');
  this.appendChild(circle);
});

A CSS @keyframes animation then scales the circle from scale(0) to scale(2.5) while fading opacity to 0. The button needs position: relative; overflow: hidden so the expanding circle is clipped to its bounds. Any previous ripple element is removed before creating a new one to avoid DOM buildup.

This is the standard pattern used by Material UI, Angular Material, and most component libraries. It gives pixel-accurate ripple origin and works on any clickable element. The CSS-only version above is lighter but always ripples from the centre.

React Ripple Button Component

The React implementation wraps the click-to-ripple pattern in a reusable component. A useCallback handler computes the ripple position from e.clientX and e.clientY relative to getBoundingClientRect(), creates a <span> element with the calculated size and offset, and appends it to the button ref. The ripple span uses the same @keyframes scale animation as the vanilla JS version.

This approach uses direct DOM manipulation inside the callback rather than React state, intentionally. Ripple animations are fire-and-forget visual feedback; managing them through useState and re-renders would add unnecessary overhead. The <span> is removed on animationend to keep the DOM clean.

To use it, import the component and wrap any content: <RippleButton>Click me</RippleButton>. The CSS is kept in a separate file (RippleButton.css) so it can be scoped or replaced with CSS modules in your project.

Vue Ripple Button Component

The Vue version uses reactive state to manage ripple instances. Each click pushes a new object ({ x, y, show: true }) to a ripples array, where x and y are calculated from the click event relative to the button's bounding rect. A v-for loop renders a <span> for each active ripple, positioned with inline top and left styles.

The ripple animation uses two stacked @keyframes: one for transform: scale(50) expansion and one for opacity fade. When animationend fires, show is set to false and Vue's <transition-group> handles removal. The mix-blend-mode: screen on the ripple span creates a bright flash effect against dark button backgrounds.

Scoped styles ensure the ripple CSS doesn't leak to other components. The component accepts a text prop for the button label and can be dropped into any Vue 2 or Vue 3 project with the Options API.

CSS Ripple Hover Effect on Buttons

The ripple hover effect uses ::before and ::after pseudo-elements positioned on top of a circular button. Each pseudo-element is a ring, a circle with a border and no fill, that expands on hover:

.ripple-btn::before,
.ripple-btn::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 50%;
  border: 2px solid rgba(190, 88, 105, .7);
  transform: scale(1);
  filter: blur(0);
}
.ripple-btn:hover::before,
.ripple-btn:hover::after {
  transform: scale(4);
  filter: blur(2px);
  border-color: transparent;
  transition: transform 1s ease, filter 2s ease;
}
.ripple-btn:hover::after {
  transition-delay: 100ms;  /* stagger for cascading ripple */
}

Three animation styles are available: Pulse fires a one-shot expansion that fades the border to transparent as it grows; Glow replaces the border with a radial-gradient that fades in and expands, creating a soft halo; Sonar loops the expansion continuously using animation: infinite, producing a radar-ping effect.

Stagger is achieved with transition-delay (Pulse and Glow) or animation-delay (Sonar) on the second and third rings. This creates cascading ripples that feel more organic. All timing, scale, blur, ring count, and colour are controlled via CSS custom properties, making the effect easy to drop into any project.

CSS vs WebGL: Which Ripple Approach to Use

CSS and WebGL target different use-cases. Choosing the right approach depends on the scale and realism you need.

CSS ripple button

Best for UI feedback: button clicks, card hovers, and link interactions. Uses radial-gradient + @keyframes, works in every browser, zero dependencies, and very lightweight.

SVG filter ripple

Best for organic image distortion. feTurbulence + feDisplacementMap create a water-like warp that can be animated with <animate>. Good for hero sections and background effects without JavaScript.

WebGL / Canvas ripple

Best for fullscreen, high-fidelity fluid simulations. Sine-wave displacement shaders compute per-pixel offset on the GPU, enabling realistic water-surface distortion at 60 fps across entire viewport transitions.

In general, start with CSS for small, contained elements. Move to SVG filters when you need organic distortion on static images. Reach for WebGL when you need real-time, full-viewport ripple transitions driven by scroll or user interaction.

CSS Ripple Button Effect vs Material Design Ripple

When most developers search for "ripple effect", they're really looking for Material Design's pressed-state feedback, the expanding circle that appears when you tap a button, list item, or card. Google's Material Design system popularised this pattern, and frameworks like Angular Material, MUI, and Vuetify ship it out of the box. Understanding the relationship between a generic CSS ripple button and a Material ripple helps you pick the right approach.

What makes Material's ripple different

A Material ripple is a touch / click feedback pattern, not a decorative animation. It originates at the exact pointer position, expands to fill the container, and fades out on release. Material 3 adds a subtle state-layer colour shift on top of the ripple to reinforce the interaction.

By contrast, a generic CSS ripple button animation can originate from the centre, loop continuously, and serve a purely decorative role, like water rings or pulsing radar circles. The underlying CSS technique (pseudo-element + radial-gradient + scale keyframes) is the same, but the intent and behaviour differ.

Building a Material-style ripple with pure CSS

You can approximate Material's pressed-state ripple without JavaScript using ::after and the :active pseudo-class:

.md-ripple {
  position: relative;
  overflow: hidden;
}

.md-ripple::after {
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(
    circle at var(--x, 50%) var(--y, 50%),
    rgba(255,255,255,.35) 0%,
    transparent 60%
  );
  transform: scale(0);
  opacity: 1;
  transition: transform .4s ease, opacity .3s ease;
}

.md-ripple:active::after {
  transform: scale(2.5);
  opacity: 0;
  transition: transform .5s ease-out, opacity .4s ease-out;
}

This gives you centred feedback with no dependencies. For a ripple that originates at the exact click position (like the real Material component), you need a small amount of JavaScript to set --x and --y custom properties on the element based on the pointer coordinates.

When to use a framework ripple vs a custom one

Framework ripple (MUI, Angular Material, Vuetify)

Use when you're already inside a Material Design system. The ripple is pre-built, handles pointer position, touch events, keyboard focus, and accessibility states for you.

Custom CSS ripple button

Use when you need a lightweight ripple on a non-Material project, such as a WordPress theme, a landing page, or any site where pulling in a full component library would be overkill. The demo above generates the exact CSS you need.

When to Use a Ripple Effect

Material Design interfaces

Ripple effects are core to Material Design button feedback. The expanding circle on click gives users immediate visual confirmation of their interaction.

Water-themed sites

Aquariums, swimming pools, diving centres, and water sports brands use ripple animations to reinforce their aquatic identity throughout the page.

Meditation apps

Calming ripple animations suit wellness and mindfulness applications. The gentle expanding rings evoke stillness and focus.

Interactive backgrounds

Subtle ripples add life to static hero sections. A gentle, continuous ripple animation can make an otherwise flat background feel dynamic and immersive.

FAQ

How do I create a ripple effect with CSS?
Use ::before and ::after pseudo-elements with a border and border-radius: 50%, then expand them on :hover with transform: scale() and filter: blur(). Add transition-delay to stagger multiple rings for a cascading ripple. For a continuous radar-ping effect, use @keyframes with animation: infinite instead of transitions. See the Ripple Hover Effect tab above to customise and generate the code.
Can CSS create a water ripple effect?
To a degree. SVG feTurbulence combined with feDisplacementMap and an <animate> element can produce organic water-like ripple distortion on images. However, CSS-only water ripples are limited to filter-based approximations. For truly realistic fluid simulation with wave propagation, WebGL with sine-wave displacement shaders is required.
What's the difference between CSS and WebGL image ripple effects?
CSS ripple button effects are limited to circular expansion (radial-gradient + scale) or SVG filter distortion (feTurbulence). They work well for UI feedback like Material Design button clicks. WebGL image ripple effects can simulate real fluid dynamics with sine-wave displacement computed per-pixel on the GPU, producing realistic water surface distortion that responds to scroll position and user interaction.

Make Your Website Stand Out with Fullscreen Effects

Add scroll transitions, parallax and animations to your site with fullPage.js. One component, 80+ effects.

Brandire
New World of Work
OW Consulting

Powering fullscreen design for Google, Sony, BBC, eBay & more

Get fullPage.js
+ Suggest Effect