Ice LogoCursor01Web design · Transition
v1.0.0Ripple Reveal
Slideshow transition using CSS mask + radial-gradient to expand slides outward from a configurable origin point with a soft liquid ripple edge
- Section
- Web design
- Kind
- Transition
- Published
- Built with
- CSS
- Version
- 1.0.0
- Tech
- CSS Mask, CSS Custom Properties, @property, CSS Animations, Vanilla JS, TypeScript
npx vanilla-motion add ripple-reveal
Published Jul 22, 2026.
02How to use
Three ways- 01Customize it
Tune every value in the app until it looks right.
- 02Copy the embed
Settings are data-* attributes on the element, in kebab-case.
<div data-ripple-reveal style="height:480px"></div> <script src="https://vanilla-motion.com/embed/ripple-reveal/1.0.0/ripple-reveal.js"></script> - 03Paste it on your site
Anywhere that takes HTML: an Embed element in Webflow or Framer, a Custom HTML block in WordPress, or straight into your page.
- 01Connect once
Add it as an MCP server (or custom connector) in Claude, ChatGPT, Cursor, VS Code or any app that supports MCP.
https://mcp.vanilla-motion.com/mcp - 02Ask for it
In your project, ask:
Add Ripple Reveal to this project. - 03What it reads
The same files are open to any tool, with or without the MCP.
- 01shadcn
In a project that uses shadcn:
npx shadcn@latest add https://vanilla-motion.com/r/ripple-reveal.json - 02Any project
Copies the source into ./vanilla-motion/ripple-reveal/ and lists the packages it needs.
npx vanilla-motion add ripple-reveal - 03Use it
import { mount } from "./vanilla-motion/ripple-reveal/ripple-reveal"; const fx = mount(document.querySelector("#my-effect"), { /* params */ }); fx.setParams({ /* change any param later */ }); fx.dispose();
03Case study
2 min readCSS mask + radial-gradient expands the incoming slide outward from a configurable origin point. A single @property --p drives the radius; a feather zone between the two gradient stops creates a soft, liquid ripple edge.
Ripple reveal
Circle expands from a configurable origin, like a drop hitting still water. The slideshow auto-advances every 5 seconds with a 1.2s reveal animation.
How it works
The key is @property --p — a registered CSS custom property that can be animated with @keyframes. Without @property, animating --p inside radial-gradient() doesn’t work because the browser can’t interpolate unregistered custom properties.
@property --p {
syntax: "<number>";
inherits: false;
initial-value: 0;
}
@keyframes ripple-reveal {
from { --p: 0; }
to { --p: 1; }
}
The active slide computes a radius from --p, then derives two edge stops for a feather zone:
.slideshow-ripple-reveal > .active {
--radius: calc(var(--p) * 150%); /* 150% ensures circle covers corners */
--edge-in: calc(var(--radius) - var(--feather) / 2);
--edge-out: calc(var(--radius) + var(--feather) / 2);
mask: radial-gradient(
circle at var(--origin-x) var(--origin-y),
#000 var(--edge-in),
transparent var(--edge-out)
);
animation: ripple-reveal var(--duration) var(--ease) forwards;
}
The --feather gap between --edge-in and --edge-out softens the expanding ring. Larger values → blurrier edge; 0 → hard circle wipe.
The origin and timing are params (data-* attributes on the embed, options in code). The class writes them to custom properties on the slideshow container:
.vm-ripple-reveal {
--vm-rr-x: 50%; /* originX: 0 = left edge, 100 = right edge */
--vm-rr-y: 50%; /* originY */
--vm-rr-feather: 20%; /* feather */
--vm-rr-duration: 1000ms; /* duration */
--vm-rr-ease: cubic-bezier(0.4, 0, 1, 1); /* ease-in: slow start → fast expand */
}
The time between slides (delay, in ms) stays in JS.
150% for the max radius guarantees the circle covers all four corners regardless of origin position. If you set originX: 0, the farthest corner is the top-right; 150% of the diagonal still clears it.
Slide z-index management
CSS animation doesn’t handle stacking — JS does. Each transition bumps a monotonically increasing zCounter so the incoming slide always paints above everything:
slides[idx].style.zIndex = String(++zCounter)
slides[idx].classList.remove('active') // force animation restart
slides[idx].classList.add('active')
The remove + add in sequence forces the browser to restart the @keyframes animation even if the slide was previously .active. After animDuration + 200ms (the duration param), the previous slide drops its .active class and falls to z-index: 0.
Ken Burns effect
Active slides get a slow scale(1.1) via a long transition:
.slideshow-ripple-reveal img {
transform: scale(1);
transition: transform 10s linear;
}
.slideshow-ripple-reveal > .active img {
transform: scale(1.1);
}
The 10s duration means the zoom is barely perceptible within a 5s slide hold, but visible as slow drift — the Ken Burns effect without JS.
Accessibility
if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
mount(el, { images })
}
When prefers-reduced-motion is set, skip mounting the slideshow entirely so slides don’t auto-advance and no animation runs.
Top-left origin
Set originX: 0 and originY: 0 (data-origin-x="0" data-origin-y="0" on the embed) — ripple expands from the top-left corner.



