# Ripple Reveal > Slideshow transition using CSS mask + radial-gradient to expand slides outward from a configurable origin point with a soft liquid ripple edge Live: https://vanilla-motion.com/ripple-reveal/ · Tuning app: https://vanilla-motion.com/ripple-reveal/app/ · Code reference: https://vanilla-motion.com/ripple-reveal/skill.md · Params: https://vanilla-motion.com/ripple-reveal/params.json CSS `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. ```css @property --p { syntax: ""; 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: ```css .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: ```css .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: ```ts 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`: ```css .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 ```ts 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. > ▶ [Live demo](https://vanilla-motion.com/ripple-reveal/embed/top-left/) · [open in the tuning app](https://vanilla-motion.com/ripple-reveal/app/)