03 / 20
All 20 in 20 CSS Ripple Effects
- 01Ripple Origin from Click Coordinates
- 02Keeping Ripples Circular in Any Box
- 03Overlapping Ripples Without Stacking
- 04Ripple Easing That Feels Like Water
- 05Compositor Only Ripples at Sixty FPS
- 06Ripple Cleanup and Memory in Long Lists
- 07Ripple on Touch Without the Tap Delay
- 08Accessible Ripples and Reduced Motion
- 09Water Surface Click Ripple
- 10Material Ink Button Ripple
- 11Pond Background with Drifting Ripples
- 12Neon Glow Ripple Ring
- 13Ripple Loading Spinner
- 14Sonar Ping Status Ripple
- 15Heartbeat Pulse Ripple
- 16Ripple Card Hover Reveal
- 17Sound Wave Ripple Visualizer
- 18Liquid Drop Impact Ripple
- 19Ripple Login Form Interaction
- 20Aurora Ripple Background
Overlapping Ripples Without Stacking
Fire rings faster than they fade and their alphas add until the surface is a solid disc. The cure is compositing, not arithmetic: cap the concurrent count, front-load the opacity decay so each ring loses most of its weight early, and blend with screen so overlap reads brighter rather than denser. One, three and eight rings run side by side.
Build UI like this from scratch: Flexbox, Grid, animations & responsive CSS
ZTM · The CSS Bootcamp · 30‑day money-back guarantee · Affiliate link
The code
<section class="rpl-03" aria-labelledby="rpl-03-title">
<input class="rpl-03__theme-input" type="checkbox" id="rpl-03-theme">
<label class="rpl-03__theme" for="rpl-03-theme">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><circle cx="12" cy="12" r="8.5"/><path d="M12 3.5a8.5 8.5 0 0 0 0 17Z" fill="currentColor" stroke="none"/></svg>
<span class="rpl-03__theme-sr">Switch between dark and light theme</span>
</label>
<div class="rpl-03__panel">
<header class="rpl-03__top">
<div>
<h1 class="rpl-03__h1" id="rpl-03-title">Edge traffic</h1>
<p class="rpl-03__sub">Live requests across 8 regions</p>
</div>
<span class="rpl-03__pill">Streaming</span>
</header>
<div class="rpl-03__field" aria-hidden="true">
<span class="rpl-03__node" style="--rpl-03-nx: 18%; --rpl-03-ny: 30%; --rpl-03-delay: 0s"></span>
<span class="rpl-03__node" style="--rpl-03-nx: 34%; --rpl-03-ny: 68%; --rpl-03-delay: 0.55s"></span>
<span class="rpl-03__node" style="--rpl-03-nx: 52%; --rpl-03-ny: 22%; --rpl-03-delay: 1.1s"></span>
<span class="rpl-03__node" style="--rpl-03-nx: 63%; --rpl-03-ny: 55%; --rpl-03-delay: 1.6s"></span>
<span class="rpl-03__node" style="--rpl-03-nx: 78%; --rpl-03-ny: 34%; --rpl-03-delay: 2.2s"></span>
<span class="rpl-03__node" style="--rpl-03-nx: 86%; --rpl-03-ny: 72%; --rpl-03-delay: 2.75s"></span>
<span class="rpl-03__node" style="--rpl-03-nx: 26%; --rpl-03-ny: 88%; --rpl-03-delay: 3.3s"></span>
<span class="rpl-03__node" style="--rpl-03-nx: 45%; --rpl-03-ny: 44%; --rpl-03-delay: 3.85s"></span>
</div>
<div class="rpl-03__stats">
<div class="rpl-03__stat"><span class="rpl-03__stat-n">41.2k</span><span class="rpl-03__stat-l">req / min</span></div>
<div class="rpl-03__stat"><span class="rpl-03__stat-n">38 ms</span><span class="rpl-03__stat-l">p50 latency</span></div>
<div class="rpl-03__stat"><span class="rpl-03__stat-n">0.02%</span><span class="rpl-03__stat-l">error rate</span></div>
</div>
</div>
</section><section class="rpl-03" aria-labelledby="rpl-03-title">
<input class="rpl-03__theme-input" type="checkbox" id="rpl-03-theme">
<label class="rpl-03__theme" for="rpl-03-theme">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><circle cx="12" cy="12" r="8.5"/><path d="M12 3.5a8.5 8.5 0 0 0 0 17Z" fill="currentColor" stroke="none"/></svg>
<span class="rpl-03__theme-sr">Switch between dark and light theme</span>
</label>
<div class="rpl-03__panel">
<header class="rpl-03__top">
<div>
<h1 class="rpl-03__h1" id="rpl-03-title">Edge traffic</h1>
<p class="rpl-03__sub">Live requests across 8 regions</p>
</div>
<span class="rpl-03__pill">Streaming</span>
</header>
<div class="rpl-03__field" aria-hidden="true">
<span class="rpl-03__node" style="--rpl-03-nx: 18%; --rpl-03-ny: 30%; --rpl-03-delay: 0s"></span>
<span class="rpl-03__node" style="--rpl-03-nx: 34%; --rpl-03-ny: 68%; --rpl-03-delay: 0.55s"></span>
<span class="rpl-03__node" style="--rpl-03-nx: 52%; --rpl-03-ny: 22%; --rpl-03-delay: 1.1s"></span>
<span class="rpl-03__node" style="--rpl-03-nx: 63%; --rpl-03-ny: 55%; --rpl-03-delay: 1.6s"></span>
<span class="rpl-03__node" style="--rpl-03-nx: 78%; --rpl-03-ny: 34%; --rpl-03-delay: 2.2s"></span>
<span class="rpl-03__node" style="--rpl-03-nx: 86%; --rpl-03-ny: 72%; --rpl-03-delay: 2.75s"></span>
<span class="rpl-03__node" style="--rpl-03-nx: 26%; --rpl-03-ny: 88%; --rpl-03-delay: 3.3s"></span>
<span class="rpl-03__node" style="--rpl-03-nx: 45%; --rpl-03-ny: 44%; --rpl-03-delay: 3.85s"></span>
</div>
<div class="rpl-03__stats">
<div class="rpl-03__stat"><span class="rpl-03__stat-n">41.2k</span><span class="rpl-03__stat-l">req / min</span></div>
<div class="rpl-03__stat"><span class="rpl-03__stat-n">38 ms</span><span class="rpl-03__stat-l">p50 latency</span></div>
<div class="rpl-03__stat"><span class="rpl-03__stat-n">0.02%</span><span class="rpl-03__stat-l">error rate</span></div>
</div>
</div>
</section>.rpl-03 {
--rpl-03-bg: #08090c;
--rpl-03-bg: oklch(0.145 0.008 260);
--rpl-03-panel: #0e1015;
--rpl-03-panel: oklch(0.185 0.010 260);
--rpl-03-line: #1e222b;
--rpl-03-line: oklch(0.265 0.012 260);
--rpl-03-fg: #eaedf3;
--rpl-03-fg: oklch(0.945 0.005 260);
--rpl-03-muted: #8b93a3;
--rpl-03-muted: oklch(0.665 0.015 260);
--rpl-03-accent: #4ee0a8;
--rpl-03-accent: oklch(0.84 0.145 165);
--rpl-03-cycle: 4.4s;
position: relative;
display: grid;
place-items: center;
min-block-size: 100vh;
min-block-size: 100svh;
padding: clamp(16px, 4vw, 48px);
background: var(--rpl-03-bg);
color: var(--rpl-03-fg);
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
box-sizing: border-box;
}
.rpl-03 *,
.rpl-03 *::before,
.rpl-03 *::after {
box-sizing: border-box;
}
.rpl-03__panel {
inline-size: 100%;
max-inline-size: 900px;
display: flex;
flex-direction: column;
gap: 20px;
padding: clamp(18px, 3vw, 28px);
border: 1px solid var(--rpl-03-line);
border-radius: 22px;
background: var(--rpl-03-panel);
}
.rpl-03__top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
}
.rpl-03__h1 {
margin: 0 0 6px;
font-size: clamp(20px, 3vw, 26px);
font-weight: 600;
letter-spacing: -0.02em;
}
.rpl-03__sub {
margin: 0;
font-size: 12.5px;
color: var(--rpl-03-muted);
}
.rpl-03__pill {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 6px 12px;
border: 1px solid color-mix(in oklab, var(--rpl-03-accent) 35%, transparent);
border-radius: 999px;
font-size: 11px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--rpl-03-accent);
}
.rpl-03__pill::before {
content: "";
inline-size: 6px;
block-size: 6px;
border-radius: 50%;
background: var(--rpl-03-accent);
}
.rpl-03__field {
position: relative;
block-size: clamp(240px, 42vh, 360px);
border-radius: 16px;
overflow: hidden;
isolation: isolate;
background: linear-gradient(oklch(0.22 0.02 200 / 0.35), transparent), repeating-linear-gradient(90deg, oklch(0.30 0.01 260 / 0.30) 0 1px, transparent 1px 68px), repeating-linear-gradient(0deg, oklch(0.30 0.01 260 / 0.30) 0 1px, transparent 1px 68px), oklch(0.165 0.010 260);
}
.rpl-03__node {
position: absolute;
left: var(--rpl-03-nx);
top: var(--rpl-03-ny);
inline-size: 8px;
block-size: 8px;
margin: -4px 0 0 -4px;
border-radius: 50%;
background: var(--rpl-03-accent);
box-shadow: 0 0 12px color-mix(in oklab, var(--rpl-03-accent) 60%, transparent);
}
/* Two rings per node, offset in time. mix-blend-mode: screen makes crossings lighten instead of stack. */
.rpl-03__node::before,
.rpl-03__node::after {
content: "";
position: absolute;
inset-block-start: 50%;
inset-inline-start: 50%;
inline-size: 300px;
aspect-ratio: 1;
margin: 0;
border: 1.5px solid var(--rpl-03-accent);
border-radius: 50%;
mix-blend-mode: screen;
animation: rpl-03-spread var(--rpl-03-cycle) cubic-bezier(0.1, 0.7, 0.3, 1) infinite;
animation-delay: var(--rpl-03-delay);
}
.rpl-03__node::after {
animation-delay: calc(var(--rpl-03-delay) + var(--rpl-03-cycle) / 2);
}
@keyframes rpl-03-spread {
from {
transform: translate(-50%, -50%) scale(0.02);
opacity: 0.75;
}
55% {
opacity: 0.18;
}
to {
transform: translate(-50%, -50%) scale(1);
opacity: 0;
}
}
.rpl-03__stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 12px;
}
.rpl-03__stat {
display: flex;
flex-direction: column;
gap: 4px;
padding: 14px 16px;
border: 1px solid var(--rpl-03-line);
border-radius: 14px;
}
.rpl-03__stat-n {
font-size: 20px;
font-weight: 600;
font-variant-numeric: tabular-nums;
letter-spacing: -0.02em;
}
.rpl-03__stat-l {
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--rpl-03-muted);
}
@media (prefers-reduced-motion: reduce) {
.rpl-03__node::before,
.rpl-03__node::after {
animation: none;
opacity: 0.22;
transform: translate(-50%, -50%) scale(0.35);
}
}
/* Counter-scheme (toggle): every colour declaration re-declared — oklch lightness mirrored, var()/color-mix() carried over so variant rules keep their own text colour, lightening blends flipped to darkening. */
.rpl-03:has(#rpl-03-theme:checked) {
--rpl-03-bg: oklch(0.850 0.008 260);
--rpl-03-panel: oklch(0.813 0.010 260);
--rpl-03-line: oklch(0.739 0.012 260);
--rpl-03-fg: oklch(0.140 0.005 260);
--rpl-03-muted: oklch(0.367 0.015 260);
--rpl-03-accent: oklch(0.204 0.145 165);
background: var(--rpl-03-bg);
color: var(--rpl-03-fg);
}
.rpl-03:has(#rpl-03-theme:checked) .rpl-03__panel {
border: 1px solid var(--rpl-03-line);
background: var(--rpl-03-panel);
}
.rpl-03:has(#rpl-03-theme:checked) .rpl-03__sub {
color: var(--rpl-03-muted);
}
.rpl-03:has(#rpl-03-theme:checked) .rpl-03__pill {
border: 1px solid color-mix(in oklab, var(--rpl-03-accent) 35%, transparent);
color: var(--rpl-03-accent);
}
.rpl-03:has(#rpl-03-theme:checked) .rpl-03__pill::before {
background: var(--rpl-03-accent);
}
.rpl-03:has(#rpl-03-theme:checked) .rpl-03__field {
background: linear-gradient(oklch(0.780 0.02 200 / 0.35), transparent), repeating-linear-gradient(90deg, oklch(0.706 0.01 260 / 0.30) 0 1px, transparent 1px 68px), repeating-linear-gradient(0deg, oklch(0.706 0.01 260 / 0.30) 0 1px, transparent 1px 68px), oklch(0.832 0.010 260);
}
.rpl-03:has(#rpl-03-theme:checked) .rpl-03__node {
background: var(--rpl-03-accent);
box-shadow: 0 0 12px color-mix(in oklab, var(--rpl-03-accent) 60%, transparent);
}
.rpl-03:has(#rpl-03-theme:checked) .rpl-03__node::before,
.rpl-03:has(#rpl-03-theme:checked) .rpl-03__node::after {
border: 1.5px solid var(--rpl-03-accent);
mix-blend-mode: multiply;
}
.rpl-03:has(#rpl-03-theme:checked) .rpl-03__stat {
border: 1px solid var(--rpl-03-line);
}
.rpl-03:has(#rpl-03-theme:checked) .rpl-03__stat-l {
color: var(--rpl-03-muted);
}
.rpl-03 {
position: relative;
}
.rpl-03__top {
padding-inline-end: 46px;
}
.rpl-03__theme-input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.rpl-03__theme {
position: absolute;
z-index: 60;
inset-block-start: 14px;
inset-inline-end: 14px;
display: grid;
place-items: center;
inline-size: 44px;
block-size: 44px;
border-radius: 999px;
border: 1px solid color-mix(in oklab, currentColor 22%, transparent);
background: color-mix(in oklab, currentColor 8%, transparent);
color: inherit;
cursor: pointer;
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
transition: background 160ms ease, border-color 160ms ease, transform 160ms ease;
}
.rpl-03__theme:hover {
background: color-mix(in oklab, currentColor 15%, transparent);
border-color: color-mix(in oklab, currentColor 38%, transparent);
}
.rpl-03__theme:active {
transform: scale(0.94);
}
.rpl-03__theme svg {
inline-size: 19px;
block-size: 19px;
display: block;
transition: transform 320ms cubic-bezier(0.2, 0.7, 0.3, 1);
}
.rpl-03__theme-input:checked + .rpl-03__theme svg {
transform: rotate(180deg);
}
.rpl-03__theme-input:focus-visible + .rpl-03__theme {
outline: 2px solid currentColor;
outline-offset: 3px;
}
.rpl-03__theme-sr {
position: absolute;
inline-size: 1px;
block-size: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
@media (prefers-reduced-motion: reduce) {
.rpl-03__theme,
.rpl-03__theme svg {
transition: none;
}
}.rpl-03 {
--rpl-03-bg: #08090c;
--rpl-03-bg: oklch(0.145 0.008 260);
--rpl-03-panel: #0e1015;
--rpl-03-panel: oklch(0.185 0.010 260);
--rpl-03-line: #1e222b;
--rpl-03-line: oklch(0.265 0.012 260);
--rpl-03-fg: #eaedf3;
--rpl-03-fg: oklch(0.945 0.005 260);
--rpl-03-muted: #8b93a3;
--rpl-03-muted: oklch(0.665 0.015 260);
--rpl-03-accent: #4ee0a8;
--rpl-03-accent: oklch(0.84 0.145 165);
--rpl-03-cycle: 4.4s;
position: relative;
display: grid;
place-items: center;
min-block-size: 100vh;
min-block-size: 100svh;
padding: clamp(16px, 4vw, 48px);
background: var(--rpl-03-bg);
color: var(--rpl-03-fg);
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
box-sizing: border-box;
}
.rpl-03 *,
.rpl-03 *::before,
.rpl-03 *::after {
box-sizing: border-box;
}
.rpl-03__panel {
inline-size: 100%;
max-inline-size: 900px;
display: flex;
flex-direction: column;
gap: 20px;
padding: clamp(18px, 3vw, 28px);
border: 1px solid var(--rpl-03-line);
border-radius: 22px;
background: var(--rpl-03-panel);
}
.rpl-03__top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
}
.rpl-03__h1 {
margin: 0 0 6px;
font-size: clamp(20px, 3vw, 26px);
font-weight: 600;
letter-spacing: -0.02em;
}
.rpl-03__sub {
margin: 0;
font-size: 12.5px;
color: var(--rpl-03-muted);
}
.rpl-03__pill {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 6px 12px;
border: 1px solid color-mix(in oklab, var(--rpl-03-accent) 35%, transparent);
border-radius: 999px;
font-size: 11px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--rpl-03-accent);
}
.rpl-03__pill::before {
content: "";
inline-size: 6px;
block-size: 6px;
border-radius: 50%;
background: var(--rpl-03-accent);
}
.rpl-03__field {
position: relative;
block-size: clamp(240px, 42vh, 360px);
border-radius: 16px;
overflow: hidden;
isolation: isolate;
background: linear-gradient(oklch(0.22 0.02 200 / 0.35), transparent), repeating-linear-gradient(90deg, oklch(0.30 0.01 260 / 0.30) 0 1px, transparent 1px 68px), repeating-linear-gradient(0deg, oklch(0.30 0.01 260 / 0.30) 0 1px, transparent 1px 68px), oklch(0.165 0.010 260);
}
.rpl-03__node {
position: absolute;
left: var(--rpl-03-nx);
top: var(--rpl-03-ny);
inline-size: 8px;
block-size: 8px;
margin: -4px 0 0 -4px;
border-radius: 50%;
background: var(--rpl-03-accent);
box-shadow: 0 0 12px color-mix(in oklab, var(--rpl-03-accent) 60%, transparent);
}
/* Two rings per node, offset in time. mix-blend-mode: screen makes crossings lighten instead of stack. */
.rpl-03__node::before,
.rpl-03__node::after {
content: "";
position: absolute;
inset-block-start: 50%;
inset-inline-start: 50%;
inline-size: 300px;
aspect-ratio: 1;
margin: 0;
border: 1.5px solid var(--rpl-03-accent);
border-radius: 50%;
mix-blend-mode: screen;
animation: rpl-03-spread var(--rpl-03-cycle) cubic-bezier(0.1, 0.7, 0.3, 1) infinite;
animation-delay: var(--rpl-03-delay);
}
.rpl-03__node::after {
animation-delay: calc(var(--rpl-03-delay) + var(--rpl-03-cycle) / 2);
}
@keyframes rpl-03-spread {
from {
transform: translate(-50%, -50%) scale(0.02);
opacity: 0.75;
}
55% {
opacity: 0.18;
}
to {
transform: translate(-50%, -50%) scale(1);
opacity: 0;
}
}
.rpl-03__stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 12px;
}
.rpl-03__stat {
display: flex;
flex-direction: column;
gap: 4px;
padding: 14px 16px;
border: 1px solid var(--rpl-03-line);
border-radius: 14px;
}
.rpl-03__stat-n {
font-size: 20px;
font-weight: 600;
font-variant-numeric: tabular-nums;
letter-spacing: -0.02em;
}
.rpl-03__stat-l {
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--rpl-03-muted);
}
@media (prefers-reduced-motion: reduce) {
.rpl-03__node::before,
.rpl-03__node::after {
animation: none;
opacity: 0.22;
transform: translate(-50%, -50%) scale(0.35);
}
}
/* Counter-scheme (toggle): every colour declaration re-declared — oklch lightness mirrored, var()/color-mix() carried over so variant rules keep their own text colour, lightening blends flipped to darkening. */
.rpl-03:has(#rpl-03-theme:checked) {
--rpl-03-bg: oklch(0.850 0.008 260);
--rpl-03-panel: oklch(0.813 0.010 260);
--rpl-03-line: oklch(0.739 0.012 260);
--rpl-03-fg: oklch(0.140 0.005 260);
--rpl-03-muted: oklch(0.367 0.015 260);
--rpl-03-accent: oklch(0.204 0.145 165);
background: var(--rpl-03-bg);
color: var(--rpl-03-fg);
}
.rpl-03:has(#rpl-03-theme:checked) .rpl-03__panel {
border: 1px solid var(--rpl-03-line);
background: var(--rpl-03-panel);
}
.rpl-03:has(#rpl-03-theme:checked) .rpl-03__sub {
color: var(--rpl-03-muted);
}
.rpl-03:has(#rpl-03-theme:checked) .rpl-03__pill {
border: 1px solid color-mix(in oklab, var(--rpl-03-accent) 35%, transparent);
color: var(--rpl-03-accent);
}
.rpl-03:has(#rpl-03-theme:checked) .rpl-03__pill::before {
background: var(--rpl-03-accent);
}
.rpl-03:has(#rpl-03-theme:checked) .rpl-03__field {
background: linear-gradient(oklch(0.780 0.02 200 / 0.35), transparent), repeating-linear-gradient(90deg, oklch(0.706 0.01 260 / 0.30) 0 1px, transparent 1px 68px), repeating-linear-gradient(0deg, oklch(0.706 0.01 260 / 0.30) 0 1px, transparent 1px 68px), oklch(0.832 0.010 260);
}
.rpl-03:has(#rpl-03-theme:checked) .rpl-03__node {
background: var(--rpl-03-accent);
box-shadow: 0 0 12px color-mix(in oklab, var(--rpl-03-accent) 60%, transparent);
}
.rpl-03:has(#rpl-03-theme:checked) .rpl-03__node::before,
.rpl-03:has(#rpl-03-theme:checked) .rpl-03__node::after {
border: 1.5px solid var(--rpl-03-accent);
mix-blend-mode: multiply;
}
.rpl-03:has(#rpl-03-theme:checked) .rpl-03__stat {
border: 1px solid var(--rpl-03-line);
}
.rpl-03:has(#rpl-03-theme:checked) .rpl-03__stat-l {
color: var(--rpl-03-muted);
}
.rpl-03 {
position: relative;
}
.rpl-03__top {
padding-inline-end: 46px;
}
.rpl-03__theme-input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.rpl-03__theme {
position: absolute;
z-index: 60;
inset-block-start: 14px;
inset-inline-end: 14px;
display: grid;
place-items: center;
inline-size: 44px;
block-size: 44px;
border-radius: 999px;
border: 1px solid color-mix(in oklab, currentColor 22%, transparent);
background: color-mix(in oklab, currentColor 8%, transparent);
color: inherit;
cursor: pointer;
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
transition: background 160ms ease, border-color 160ms ease, transform 160ms ease;
}
.rpl-03__theme:hover {
background: color-mix(in oklab, currentColor 15%, transparent);
border-color: color-mix(in oklab, currentColor 38%, transparent);
}
.rpl-03__theme:active {
transform: scale(0.94);
}
.rpl-03__theme svg {
inline-size: 19px;
block-size: 19px;
display: block;
transition: transform 320ms cubic-bezier(0.2, 0.7, 0.3, 1);
}
.rpl-03__theme-input:checked + .rpl-03__theme svg {
transform: rotate(180deg);
}
.rpl-03__theme-input:focus-visible + .rpl-03__theme {
outline: 2px solid currentColor;
outline-offset: 3px;
}
.rpl-03__theme-sr {
position: absolute;
inline-size: 1px;
block-size: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
@media (prefers-reduced-motion: reduce) {
.rpl-03__theme,
.rpl-03__theme svg {
transition: none;
}
}Here's a working CSS Ripple Effect from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Overlapping Ripples Without Stacking
Source: https://codefronts.com/motion/css-ripple-designs/overlapping-ripples-without-stacking/
Fire rings faster than they fade and their alphas add until the surface is a solid disc. The cure is compositing, not arithmetic: cap the concurrent count, front-load the opacity decay so each ring loses most of its weight early, and blend with screen so overlap reads brighter rather than denser. One, three and eight rings run side by side.
## HTML
```html
<section class="rpl-03" aria-labelledby="rpl-03-title">
<input class="rpl-03__theme-input" type="checkbox" id="rpl-03-theme">
<label class="rpl-03__theme" for="rpl-03-theme">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><circle cx="12" cy="12" r="8.5"/><path d="M12 3.5a8.5 8.5 0 0 0 0 17Z" fill="currentColor" stroke="none"/></svg>
<span class="rpl-03__theme-sr">Switch between dark and light theme</span>
</label>
<div class="rpl-03__panel">
<header class="rpl-03__top">
<div>
<h1 class="rpl-03__h1" id="rpl-03-title">Edge traffic</h1>
<p class="rpl-03__sub">Live requests across 8 regions</p>
</div>
<span class="rpl-03__pill">Streaming</span>
</header>
<div class="rpl-03__field" aria-hidden="true">
<span class="rpl-03__node" style="--rpl-03-nx: 18%; --rpl-03-ny: 30%; --rpl-03-delay: 0s"></span>
<span class="rpl-03__node" style="--rpl-03-nx: 34%; --rpl-03-ny: 68%; --rpl-03-delay: 0.55s"></span>
<span class="rpl-03__node" style="--rpl-03-nx: 52%; --rpl-03-ny: 22%; --rpl-03-delay: 1.1s"></span>
<span class="rpl-03__node" style="--rpl-03-nx: 63%; --rpl-03-ny: 55%; --rpl-03-delay: 1.6s"></span>
<span class="rpl-03__node" style="--rpl-03-nx: 78%; --rpl-03-ny: 34%; --rpl-03-delay: 2.2s"></span>
<span class="rpl-03__node" style="--rpl-03-nx: 86%; --rpl-03-ny: 72%; --rpl-03-delay: 2.75s"></span>
<span class="rpl-03__node" style="--rpl-03-nx: 26%; --rpl-03-ny: 88%; --rpl-03-delay: 3.3s"></span>
<span class="rpl-03__node" style="--rpl-03-nx: 45%; --rpl-03-ny: 44%; --rpl-03-delay: 3.85s"></span>
</div>
<div class="rpl-03__stats">
<div class="rpl-03__stat"><span class="rpl-03__stat-n">41.2k</span><span class="rpl-03__stat-l">req / min</span></div>
<div class="rpl-03__stat"><span class="rpl-03__stat-n">38 ms</span><span class="rpl-03__stat-l">p50 latency</span></div>
<div class="rpl-03__stat"><span class="rpl-03__stat-n">0.02%</span><span class="rpl-03__stat-l">error rate</span></div>
</div>
</div>
</section>
```
## CSS
```css
.rpl-03 {
--rpl-03-bg: #08090c;
--rpl-03-bg: oklch(0.145 0.008 260);
--rpl-03-panel: #0e1015;
--rpl-03-panel: oklch(0.185 0.010 260);
--rpl-03-line: #1e222b;
--rpl-03-line: oklch(0.265 0.012 260);
--rpl-03-fg: #eaedf3;
--rpl-03-fg: oklch(0.945 0.005 260);
--rpl-03-muted: #8b93a3;
--rpl-03-muted: oklch(0.665 0.015 260);
--rpl-03-accent: #4ee0a8;
--rpl-03-accent: oklch(0.84 0.145 165);
--rpl-03-cycle: 4.4s;
position: relative;
display: grid;
place-items: center;
min-block-size: 100vh;
min-block-size: 100svh;
padding: clamp(16px, 4vw, 48px);
background: var(--rpl-03-bg);
color: var(--rpl-03-fg);
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
box-sizing: border-box;
}
.rpl-03 *,
.rpl-03 *::before,
.rpl-03 *::after {
box-sizing: border-box;
}
.rpl-03__panel {
inline-size: 100%;
max-inline-size: 900px;
display: flex;
flex-direction: column;
gap: 20px;
padding: clamp(18px, 3vw, 28px);
border: 1px solid var(--rpl-03-line);
border-radius: 22px;
background: var(--rpl-03-panel);
}
.rpl-03__top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
}
.rpl-03__h1 {
margin: 0 0 6px;
font-size: clamp(20px, 3vw, 26px);
font-weight: 600;
letter-spacing: -0.02em;
}
.rpl-03__sub {
margin: 0;
font-size: 12.5px;
color: var(--rpl-03-muted);
}
.rpl-03__pill {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 6px 12px;
border: 1px solid color-mix(in oklab, var(--rpl-03-accent) 35%, transparent);
border-radius: 999px;
font-size: 11px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--rpl-03-accent);
}
.rpl-03__pill::before {
content: "";
inline-size: 6px;
block-size: 6px;
border-radius: 50%;
background: var(--rpl-03-accent);
}
.rpl-03__field {
position: relative;
block-size: clamp(240px, 42vh, 360px);
border-radius: 16px;
overflow: hidden;
isolation: isolate;
background: linear-gradient(oklch(0.22 0.02 200 / 0.35), transparent), repeating-linear-gradient(90deg, oklch(0.30 0.01 260 / 0.30) 0 1px, transparent 1px 68px), repeating-linear-gradient(0deg, oklch(0.30 0.01 260 / 0.30) 0 1px, transparent 1px 68px), oklch(0.165 0.010 260);
}
.rpl-03__node {
position: absolute;
left: var(--rpl-03-nx);
top: var(--rpl-03-ny);
inline-size: 8px;
block-size: 8px;
margin: -4px 0 0 -4px;
border-radius: 50%;
background: var(--rpl-03-accent);
box-shadow: 0 0 12px color-mix(in oklab, var(--rpl-03-accent) 60%, transparent);
}
/* Two rings per node, offset in time. mix-blend-mode: screen makes crossings lighten instead of stack. */
.rpl-03__node::before,
.rpl-03__node::after {
content: "";
position: absolute;
inset-block-start: 50%;
inset-inline-start: 50%;
inline-size: 300px;
aspect-ratio: 1;
margin: 0;
border: 1.5px solid var(--rpl-03-accent);
border-radius: 50%;
mix-blend-mode: screen;
animation: rpl-03-spread var(--rpl-03-cycle) cubic-bezier(0.1, 0.7, 0.3, 1) infinite;
animation-delay: var(--rpl-03-delay);
}
.rpl-03__node::after {
animation-delay: calc(var(--rpl-03-delay) + var(--rpl-03-cycle) / 2);
}
@keyframes rpl-03-spread {
from {
transform: translate(-50%, -50%) scale(0.02);
opacity: 0.75;
}
55% {
opacity: 0.18;
}
to {
transform: translate(-50%, -50%) scale(1);
opacity: 0;
}
}
.rpl-03__stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 12px;
}
.rpl-03__stat {
display: flex;
flex-direction: column;
gap: 4px;
padding: 14px 16px;
border: 1px solid var(--rpl-03-line);
border-radius: 14px;
}
.rpl-03__stat-n {
font-size: 20px;
font-weight: 600;
font-variant-numeric: tabular-nums;
letter-spacing: -0.02em;
}
.rpl-03__stat-l {
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--rpl-03-muted);
}
@media (prefers-reduced-motion: reduce) {
.rpl-03__node::before,
.rpl-03__node::after {
animation: none;
opacity: 0.22;
transform: translate(-50%, -50%) scale(0.35);
}
}
/* Counter-scheme (toggle): every colour declaration re-declared — oklch lightness mirrored, var()/color-mix() carried over so variant rules keep their own text colour, lightening blends flipped to darkening. */
.rpl-03:has(#rpl-03-theme:checked) {
--rpl-03-bg: oklch(0.850 0.008 260);
--rpl-03-panel: oklch(0.813 0.010 260);
--rpl-03-line: oklch(0.739 0.012 260);
--rpl-03-fg: oklch(0.140 0.005 260);
--rpl-03-muted: oklch(0.367 0.015 260);
--rpl-03-accent: oklch(0.204 0.145 165);
background: var(--rpl-03-bg);
color: var(--rpl-03-fg);
}
.rpl-03:has(#rpl-03-theme:checked) .rpl-03__panel {
border: 1px solid var(--rpl-03-line);
background: var(--rpl-03-panel);
}
.rpl-03:has(#rpl-03-theme:checked) .rpl-03__sub {
color: var(--rpl-03-muted);
}
.rpl-03:has(#rpl-03-theme:checked) .rpl-03__pill {
border: 1px solid color-mix(in oklab, var(--rpl-03-accent) 35%, transparent);
color: var(--rpl-03-accent);
}
.rpl-03:has(#rpl-03-theme:checked) .rpl-03__pill::before {
background: var(--rpl-03-accent);
}
.rpl-03:has(#rpl-03-theme:checked) .rpl-03__field {
background: linear-gradient(oklch(0.780 0.02 200 / 0.35), transparent), repeating-linear-gradient(90deg, oklch(0.706 0.01 260 / 0.30) 0 1px, transparent 1px 68px), repeating-linear-gradient(0deg, oklch(0.706 0.01 260 / 0.30) 0 1px, transparent 1px 68px), oklch(0.832 0.010 260);
}
.rpl-03:has(#rpl-03-theme:checked) .rpl-03__node {
background: var(--rpl-03-accent);
box-shadow: 0 0 12px color-mix(in oklab, var(--rpl-03-accent) 60%, transparent);
}
.rpl-03:has(#rpl-03-theme:checked) .rpl-03__node::before,
.rpl-03:has(#rpl-03-theme:checked) .rpl-03__node::after {
border: 1.5px solid var(--rpl-03-accent);
mix-blend-mode: multiply;
}
.rpl-03:has(#rpl-03-theme:checked) .rpl-03__stat {
border: 1px solid var(--rpl-03-line);
}
.rpl-03:has(#rpl-03-theme:checked) .rpl-03__stat-l {
color: var(--rpl-03-muted);
}
.rpl-03 {
position: relative;
}
.rpl-03__top {
padding-inline-end: 46px;
}
.rpl-03__theme-input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.rpl-03__theme {
position: absolute;
z-index: 60;
inset-block-start: 14px;
inset-inline-end: 14px;
display: grid;
place-items: center;
inline-size: 44px;
block-size: 44px;
border-radius: 999px;
border: 1px solid color-mix(in oklab, currentColor 22%, transparent);
background: color-mix(in oklab, currentColor 8%, transparent);
color: inherit;
cursor: pointer;
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
transition: background 160ms ease, border-color 160ms ease, transform 160ms ease;
}
.rpl-03__theme:hover {
background: color-mix(in oklab, currentColor 15%, transparent);
border-color: color-mix(in oklab, currentColor 38%, transparent);
}
.rpl-03__theme:active {
transform: scale(0.94);
}
.rpl-03__theme svg {
inline-size: 19px;
block-size: 19px;
display: block;
transition: transform 320ms cubic-bezier(0.2, 0.7, 0.3, 1);
}
.rpl-03__theme-input:checked + .rpl-03__theme svg {
transform: rotate(180deg);
}
.rpl-03__theme-input:focus-visible + .rpl-03__theme {
outline: 2px solid currentColor;
outline-offset: 3px;
}
.rpl-03__theme-sr {
position: absolute;
inline-size: 1px;
block-size: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
@media (prefers-reduced-motion: reduce) {
.rpl-03__theme,
.rpl-03__theme svg {
transition: none;
}
}
```Here's a working CSS Ripple Effect from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Overlapping Ripples Without Stacking
Source: https://codefronts.com/motion/css-ripple-designs/overlapping-ripples-without-stacking/
Fire rings faster than they fade and their alphas add until the surface is a solid disc. The cure is compositing, not arithmetic: cap the concurrent count, front-load the opacity decay so each ring loses most of its weight early, and blend with screen so overlap reads brighter rather than denser. One, three and eight rings run side by side.
## HTML
```html
<section class="rpl-03" aria-labelledby="rpl-03-title">
<input class="rpl-03__theme-input" type="checkbox" id="rpl-03-theme">
<label class="rpl-03__theme" for="rpl-03-theme">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><circle cx="12" cy="12" r="8.5"/><path d="M12 3.5a8.5 8.5 0 0 0 0 17Z" fill="currentColor" stroke="none"/></svg>
<span class="rpl-03__theme-sr">Switch between dark and light theme</span>
</label>
<div class="rpl-03__panel">
<header class="rpl-03__top">
<div>
<h1 class="rpl-03__h1" id="rpl-03-title">Edge traffic</h1>
<p class="rpl-03__sub">Live requests across 8 regions</p>
</div>
<span class="rpl-03__pill">Streaming</span>
</header>
<div class="rpl-03__field" aria-hidden="true">
<span class="rpl-03__node" style="--rpl-03-nx: 18%; --rpl-03-ny: 30%; --rpl-03-delay: 0s"></span>
<span class="rpl-03__node" style="--rpl-03-nx: 34%; --rpl-03-ny: 68%; --rpl-03-delay: 0.55s"></span>
<span class="rpl-03__node" style="--rpl-03-nx: 52%; --rpl-03-ny: 22%; --rpl-03-delay: 1.1s"></span>
<span class="rpl-03__node" style="--rpl-03-nx: 63%; --rpl-03-ny: 55%; --rpl-03-delay: 1.6s"></span>
<span class="rpl-03__node" style="--rpl-03-nx: 78%; --rpl-03-ny: 34%; --rpl-03-delay: 2.2s"></span>
<span class="rpl-03__node" style="--rpl-03-nx: 86%; --rpl-03-ny: 72%; --rpl-03-delay: 2.75s"></span>
<span class="rpl-03__node" style="--rpl-03-nx: 26%; --rpl-03-ny: 88%; --rpl-03-delay: 3.3s"></span>
<span class="rpl-03__node" style="--rpl-03-nx: 45%; --rpl-03-ny: 44%; --rpl-03-delay: 3.85s"></span>
</div>
<div class="rpl-03__stats">
<div class="rpl-03__stat"><span class="rpl-03__stat-n">41.2k</span><span class="rpl-03__stat-l">req / min</span></div>
<div class="rpl-03__stat"><span class="rpl-03__stat-n">38 ms</span><span class="rpl-03__stat-l">p50 latency</span></div>
<div class="rpl-03__stat"><span class="rpl-03__stat-n">0.02%</span><span class="rpl-03__stat-l">error rate</span></div>
</div>
</div>
</section>
```
## CSS
```css
.rpl-03 {
--rpl-03-bg: #08090c;
--rpl-03-bg: oklch(0.145 0.008 260);
--rpl-03-panel: #0e1015;
--rpl-03-panel: oklch(0.185 0.010 260);
--rpl-03-line: #1e222b;
--rpl-03-line: oklch(0.265 0.012 260);
--rpl-03-fg: #eaedf3;
--rpl-03-fg: oklch(0.945 0.005 260);
--rpl-03-muted: #8b93a3;
--rpl-03-muted: oklch(0.665 0.015 260);
--rpl-03-accent: #4ee0a8;
--rpl-03-accent: oklch(0.84 0.145 165);
--rpl-03-cycle: 4.4s;
position: relative;
display: grid;
place-items: center;
min-block-size: 100vh;
min-block-size: 100svh;
padding: clamp(16px, 4vw, 48px);
background: var(--rpl-03-bg);
color: var(--rpl-03-fg);
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
box-sizing: border-box;
}
.rpl-03 *,
.rpl-03 *::before,
.rpl-03 *::after {
box-sizing: border-box;
}
.rpl-03__panel {
inline-size: 100%;
max-inline-size: 900px;
display: flex;
flex-direction: column;
gap: 20px;
padding: clamp(18px, 3vw, 28px);
border: 1px solid var(--rpl-03-line);
border-radius: 22px;
background: var(--rpl-03-panel);
}
.rpl-03__top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
}
.rpl-03__h1 {
margin: 0 0 6px;
font-size: clamp(20px, 3vw, 26px);
font-weight: 600;
letter-spacing: -0.02em;
}
.rpl-03__sub {
margin: 0;
font-size: 12.5px;
color: var(--rpl-03-muted);
}
.rpl-03__pill {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 6px 12px;
border: 1px solid color-mix(in oklab, var(--rpl-03-accent) 35%, transparent);
border-radius: 999px;
font-size: 11px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--rpl-03-accent);
}
.rpl-03__pill::before {
content: "";
inline-size: 6px;
block-size: 6px;
border-radius: 50%;
background: var(--rpl-03-accent);
}
.rpl-03__field {
position: relative;
block-size: clamp(240px, 42vh, 360px);
border-radius: 16px;
overflow: hidden;
isolation: isolate;
background: linear-gradient(oklch(0.22 0.02 200 / 0.35), transparent), repeating-linear-gradient(90deg, oklch(0.30 0.01 260 / 0.30) 0 1px, transparent 1px 68px), repeating-linear-gradient(0deg, oklch(0.30 0.01 260 / 0.30) 0 1px, transparent 1px 68px), oklch(0.165 0.010 260);
}
.rpl-03__node {
position: absolute;
left: var(--rpl-03-nx);
top: var(--rpl-03-ny);
inline-size: 8px;
block-size: 8px;
margin: -4px 0 0 -4px;
border-radius: 50%;
background: var(--rpl-03-accent);
box-shadow: 0 0 12px color-mix(in oklab, var(--rpl-03-accent) 60%, transparent);
}
/* Two rings per node, offset in time. mix-blend-mode: screen makes crossings lighten instead of stack. */
.rpl-03__node::before,
.rpl-03__node::after {
content: "";
position: absolute;
inset-block-start: 50%;
inset-inline-start: 50%;
inline-size: 300px;
aspect-ratio: 1;
margin: 0;
border: 1.5px solid var(--rpl-03-accent);
border-radius: 50%;
mix-blend-mode: screen;
animation: rpl-03-spread var(--rpl-03-cycle) cubic-bezier(0.1, 0.7, 0.3, 1) infinite;
animation-delay: var(--rpl-03-delay);
}
.rpl-03__node::after {
animation-delay: calc(var(--rpl-03-delay) + var(--rpl-03-cycle) / 2);
}
@keyframes rpl-03-spread {
from {
transform: translate(-50%, -50%) scale(0.02);
opacity: 0.75;
}
55% {
opacity: 0.18;
}
to {
transform: translate(-50%, -50%) scale(1);
opacity: 0;
}
}
.rpl-03__stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 12px;
}
.rpl-03__stat {
display: flex;
flex-direction: column;
gap: 4px;
padding: 14px 16px;
border: 1px solid var(--rpl-03-line);
border-radius: 14px;
}
.rpl-03__stat-n {
font-size: 20px;
font-weight: 600;
font-variant-numeric: tabular-nums;
letter-spacing: -0.02em;
}
.rpl-03__stat-l {
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--rpl-03-muted);
}
@media (prefers-reduced-motion: reduce) {
.rpl-03__node::before,
.rpl-03__node::after {
animation: none;
opacity: 0.22;
transform: translate(-50%, -50%) scale(0.35);
}
}
/* Counter-scheme (toggle): every colour declaration re-declared — oklch lightness mirrored, var()/color-mix() carried over so variant rules keep their own text colour, lightening blends flipped to darkening. */
.rpl-03:has(#rpl-03-theme:checked) {
--rpl-03-bg: oklch(0.850 0.008 260);
--rpl-03-panel: oklch(0.813 0.010 260);
--rpl-03-line: oklch(0.739 0.012 260);
--rpl-03-fg: oklch(0.140 0.005 260);
--rpl-03-muted: oklch(0.367 0.015 260);
--rpl-03-accent: oklch(0.204 0.145 165);
background: var(--rpl-03-bg);
color: var(--rpl-03-fg);
}
.rpl-03:has(#rpl-03-theme:checked) .rpl-03__panel {
border: 1px solid var(--rpl-03-line);
background: var(--rpl-03-panel);
}
.rpl-03:has(#rpl-03-theme:checked) .rpl-03__sub {
color: var(--rpl-03-muted);
}
.rpl-03:has(#rpl-03-theme:checked) .rpl-03__pill {
border: 1px solid color-mix(in oklab, var(--rpl-03-accent) 35%, transparent);
color: var(--rpl-03-accent);
}
.rpl-03:has(#rpl-03-theme:checked) .rpl-03__pill::before {
background: var(--rpl-03-accent);
}
.rpl-03:has(#rpl-03-theme:checked) .rpl-03__field {
background: linear-gradient(oklch(0.780 0.02 200 / 0.35), transparent), repeating-linear-gradient(90deg, oklch(0.706 0.01 260 / 0.30) 0 1px, transparent 1px 68px), repeating-linear-gradient(0deg, oklch(0.706 0.01 260 / 0.30) 0 1px, transparent 1px 68px), oklch(0.832 0.010 260);
}
.rpl-03:has(#rpl-03-theme:checked) .rpl-03__node {
background: var(--rpl-03-accent);
box-shadow: 0 0 12px color-mix(in oklab, var(--rpl-03-accent) 60%, transparent);
}
.rpl-03:has(#rpl-03-theme:checked) .rpl-03__node::before,
.rpl-03:has(#rpl-03-theme:checked) .rpl-03__node::after {
border: 1.5px solid var(--rpl-03-accent);
mix-blend-mode: multiply;
}
.rpl-03:has(#rpl-03-theme:checked) .rpl-03__stat {
border: 1px solid var(--rpl-03-line);
}
.rpl-03:has(#rpl-03-theme:checked) .rpl-03__stat-l {
color: var(--rpl-03-muted);
}
.rpl-03 {
position: relative;
}
.rpl-03__top {
padding-inline-end: 46px;
}
.rpl-03__theme-input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.rpl-03__theme {
position: absolute;
z-index: 60;
inset-block-start: 14px;
inset-inline-end: 14px;
display: grid;
place-items: center;
inline-size: 44px;
block-size: 44px;
border-radius: 999px;
border: 1px solid color-mix(in oklab, currentColor 22%, transparent);
background: color-mix(in oklab, currentColor 8%, transparent);
color: inherit;
cursor: pointer;
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
transition: background 160ms ease, border-color 160ms ease, transform 160ms ease;
}
.rpl-03__theme:hover {
background: color-mix(in oklab, currentColor 15%, transparent);
border-color: color-mix(in oklab, currentColor 38%, transparent);
}
.rpl-03__theme:active {
transform: scale(0.94);
}
.rpl-03__theme svg {
inline-size: 19px;
block-size: 19px;
display: block;
transition: transform 320ms cubic-bezier(0.2, 0.7, 0.3, 1);
}
.rpl-03__theme-input:checked + .rpl-03__theme svg {
transform: rotate(180deg);
}
.rpl-03__theme-input:focus-visible + .rpl-03__theme {
outline: 2px solid currentColor;
outline-offset: 3px;
}
.rpl-03__theme-sr {
position: absolute;
inline-size: 1px;
block-size: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
@media (prefers-reduced-motion: reduce) {
.rpl-03__theme,
.rpl-03__theme svg {
transition: none;
}
}
```How this works
Alpha compositing is not addition, but it looks close enough to hurt. Two rings at 0.3 alpha over each other resolve to 0.51, three to 0.66, five to 0.83. By the fifth overlapping ring you are looking at a nearly opaque shape, and the ripple has stopped reading as motion — it reads as the surface changing colour. This is why a rapid-tap ripple degenerates into a blob, and why raising the duration makes it worse rather than better.
Cap the count before you touch the colours. The single highest-leverage fix is a budget: a small fixed number of rings may be alive at once, and a new one either replaces the oldest or is dropped. Three is a good default for a button, six for a large surface. The eight-ring panel here exists to show that past a certain density no amount of blending saves it — you can see the centre going flat where the origins cluster.
Blend so overlap lightens. mix-blend-mode: screen inverts, multiplies and inverts back, which means it can never darken: white stays white, black is a no-op, and two mid-tone rings resolve brighter than either. Overlap then reads as energy concentrating rather than paint accumulating. Pair it with isolation: isolate on the frame — without it the blend reaches through to whatever is behind the container, and the effect changes depending on what the ripple happens to be sitting on.
Front-load the fade. Give the opacity keyframe most of its drop in the first third: full at 12 percent, a quarter by 45 percent, gone by the end. The ring keeps expanding long after it has visually thinned, which both matches real displacement and guarantees that the next ring in the queue lands on something nearly transparent. Two rings can then coexist without a visible seam, which a linear fade will not give you.
Make it yours
- Change the ring count per panel by adding or removing a ring element; the
animation-delaysteps are set per ring, so the rhythm stays even. - Swap
mix-blend-mode: screenforplus-lighteron the dark panel for a harder, more neon accumulation. - Lower
--rpl-03-alphafrom 0.34 to 0.2 for a subtler surface where eight rings still read as separate. - Change
--rpl-03-inkper panel to make the three densities visually distinct rather than three copies of one colour. - Set
--rpl-03-durto 1.6s to widen the gap between rings without changing the count. - Move the origins by editing each ring's
--rpl-03-oxand--rpl-03-oyso the cluster spreads or tightens.
Gotchas — read before shipping
- Overlapping translucent rings composite toward opaque. Five rings at 0.3 alpha are effectively a solid disc, which is why fast tapping turns a ripple into a blob.
mix-blend-modewithoutisolation: isolateblends against everything behind the container, so the same ripple looks different on different backgrounds.plus-lightersaturates to white fast. On a light ground it wipes the surface out entirely; keep it for dark panels.- Screen blending makes a ring nearly invisible on a white ground, since screen cannot darken. On light surfaces cap the count and use a normal blend with a dark ink instead.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 15.4+ | 121+ | 111+ |
oklch() and the :has() toggle set the floor. mix-blend-mode (Chrome 41 / Safari 8 / Firefox 32) and isolation are far older than that, so the compositing lesson itself runs well below this floor; plus-lighter is the one modern outlier and it is offered as an optional swap rather than shipped. Engines without oklch() take the sRGB hex declared first; engines without :has() keep the base scheme and every panel still animates.