Pulse Rings
Rings ripple out from a live indicator at an even pace, like a signal broadcasting from the centre.
Live
Create a pulsing rings indicator in pure CSS. Centre a small badge with a dot and a label. Behind it, stack three absolutely positioned rings (1.5px accent border, border-radius 50%) of the same size. Animate each with a 3s infinite keyframe from scale(0.4) and opacity 0.8 to scale(3) and opacity 0, with cubic-bezier(0.16, 1, 0.3, 1) so they burst out then drift. Offset them with animation-delay 0s, 1s, and 2s so a new ring always starts as the last fades. Pause off screen, and under prefers-reduced-motion show one static ring. The label reads "Live".
Source
'use client'
import { useEffect, useRef } from 'react'
import s from './demo.module.css'
export default function PulseRings({ labels }: { labels: Record<string, string> }) {
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
const el = ref.current
if (!el) return
const io = new IntersectionObserver(([e]) => el.toggleAttribute('data-paused', !e.isIntersecting))
io.observe(el)
return () => io.disconnect()
}, [])
return (
<div ref={ref} className={s.wrap}>
{[0, 1, 2].map((n) => (
<span key={n} className={s.ring} style={{ animationDelay: `${n}s` }} aria-hidden="true" />
))}
<span className={s.badge}>
<span className={s.dot} aria-hidden="true" />
{labels.label}
</span>
</div>
)
}
.wrap {
position: relative;
display: grid;
place-items: center;
width: 80px;
height: 80px;
}
.ring {
position: absolute;
inset: 0;
border: 1.5px solid var(--stage-accent);
border-radius: 50%;
opacity: 0;
animation: pulse 3s var(--ease-out) infinite;
}
.wrap[data-paused] .ring { animation-play-state: paused; }
@keyframes pulse {
from { transform: scale(0.4); opacity: 0.8; }
to { transform: scale(3); opacity: 0; }
}
.badge {
position: relative;
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border-radius: 999px;
background: var(--stage-accent);
color: var(--stage-accent-ink);
font: 600 var(--text-xs) / 1 var(--font-mono);
letter-spacing: 0.08em;
text-transform: uppercase;
white-space: nowrap;
}
.dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
}
@media (prefers-reduced-motion: reduce) {
.ring { animation: none; }
.ring:first-child { opacity: 0.5; transform: scale(1.6); }
}