Atmosphere✨ Premium

Pulse Ring

Five concentric CSS rings expand and fade to a simulated beat — a pulsing visual indicator in pure CSS and JS, no canvas needed.

JSBeat

You'll get access to the interactive demo with a free account.

This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Atmosphere has 57 effects, including 10 free. Explore the category →

3 usage examples

Hero / Landing — Music streaming service — Pulse Ring example 1

① Hero / Landing — Music streaming service

WhenAt the top of an audio app or podcast service page — the ring visually signals a live stream or that the service is listening.
WhyThe steady ring cadence mimics a heartbeat or BPM, immediately anchoring the product’s audio identity without requiring any sound.
SettingsContainer enlarged to 180 × 180 px in .pulse-ring-container, duration reduced to 800 ms (replace 1000 with 800 in progress = elapsed / 800), indigo #6366f1 kept.
Product component — Audio indicator widget in a dashboard — Pulse Ring example 2

② Product component — Audio indicator widget in a dashboard

WhenInside a dashboard card when an audio source is active — open microphone, ongoing conference, active recording.
WhyThe pulsing ring replaces a plain blinking dot: it conveys the same information (active) with depth and rhythm, without distracting from other metrics.
SettingsContainer shrunk to 64 × 64 px, border-width lowered to 2 px, duration extended to 1 200 ms, color changed to #22c55e (green = active) in the CSS.
Conversion micro-interaction — Account activation confirmation — Pulse Ring example 3

③ Conversion micro-interaction — Account activation confirmation

WhenThe moment the user completes sign-up or activates a subscription — the welcome screen or confirmation modal.
WhyExpanding concentric rings visually signal activation without taking over — subtle enough to stay in the background, rhythmic enough to mark the moment and reinforce the sense of success.
SettingsColor #d946ef (bright purple) for a celebratory touch, duration at 900 ms, 5-ring pool preserved, tempo accelerated by lowering beatInterval to 350 ms.

How it works

A pool of five div.pulse-ring elements is created dynamically on init (in addition to the five already in the source HTML). Each ring is a pure CSS circle: border: 3px solid #6366f1 on a transparent background, border-radius: 50%, opacity: 0 by default. Beat detection (detectBeat) computes a variable interval: 500 + Math.sin(time × 0.001) × 100 ms, producing ~1.7 to 2.5 beats per second — equivalent to a simulated tempo of 100 to 150 BPM.

On each detected beat, the first inactive ring in the pool is assigned active = true and a timestamped startTime. Every requestAnimationFrame tick, the function computes a linear progress value: progress = (time − startTime) / 1000 over a one-second window. Opacity drops from 1 to 0 and scale rises from 1× to 2× — driven entirely by style.opacity and style.transform written each frame. No CSS @keyframes are used.

The interpolation is strictly linear: no ease-in or ease-out. The starting diameter is set by width: 120px; height: 120px on .pulse-ring-container, each ring beginning at inset: 0 (120 × 120 px) and expanding to 240 × 240 px at progress = 1 before resetting.

The animation runs in a shared requestAnimationFrame loop alongside up to nine other audio visualizers in the same IIFE (frequency bars, circular visualizer, waveform, particle burst…). All share the same global lastBeat variable, keeping every visualizer synchronized to the same simulated tempo.

Accessibility

  • prefers-reduced-motion absent from the code — the IIFE checks neither the CSS media query nor window.matchMedia: the animation runs regardless of OS preference. For production use, add @media (prefers-reduced-motion: reduce) { .pulse-ring { display: none; } } or read matchMedia to stop the rAF.
  • No aria-* attributes on the container or rings — the effect is invisible to screen readers. Add aria-hidden="true" on .pulse-ring-container if the effect is purely decorative.
  • No keyboard interaction — the effect is passive and decorative, which is appropriate for an audio indicator. If the container receives a tabindex, remove it or hide the element from assistive technology.
  • Ring contrast (#6366f1 on transparent): a decorative non-text element, so WCAG 1.4.3 does not apply. If the ring serves as a functional indicator (active/inactive signal), WCAG 1.4.11 (ratio ≥ 3:1 between ring and actual background) applies.
  • No role or aria-label on the container — add these if the animation conveys meaningful information (e.g. beat detected, signal active).

Browser compatibility

Requires requestAnimationFrame, document.createElement, Array.prototype.find, and the CSS inset property — covered by all modern browsers.

Chrome 87+✓ Full
Firefox 87+✓ Full
Safari 14.1+✓ Full
Edge 87+✓ Full
Mobile iOS 15+✓ Full
Android Chrome✓ Full

Without <code>requestAnimationFrame</code> (very old browsers), the loop never starts and rings stay invisible (<code>opacity: 0</code>) — no fatal JS error, the page remains functional. <code>inset</code> requires Chrome 87+, Firefox 87+, Safari 14.1+; <code>transform: scale()</code> is universal.

The code

HTML structure to paste into your page (CSS + JS available with a premium account):

index.html — structure
<div class="pulse-ring-container" id="pulseRingContainer">
  <div class="pulse-ring"></div>
  <div class="pulse-ring"></div>
  <div class="pulse-ring"></div>
  <div class="pulse-ring"></div>
  <div class="pulse-ring"></div>
  <!-- JS adds 5 more rings on init (only those are animated) -->
</div>
🔒 Unlock the full code — from €2.99 the first month

Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
border: 3px solid #6366f1 (CSS .pulse-ring) 3px solid #6366f1 Ring color and width. Swap the hex value for your brand color; increase to 4–6 px for a bolder stroke.
width / height (CSS .pulse-ring-container) 120px × 120px Ring starting diameter. Go up to 180–240 px for hero use; shrink to 60–80 px for a compact indicator inside a card.
elapsed / 1000 (JS animatePulseRing) 1000 ms Duration of each ring animation. Drop to 600 ms for a snappier beat; raise to 1 500–2 000 ms for a slow, hypnotic pulse.
scale(${1 + progress}) (JS animatePulseRing) 2× final scale (delta 1) Maximum ring expansion. Replace with scale(${1 + 2 * progress}) to triple the final size; 1 + 0.5 * progress for a subtler expand.
500 + Math.sin(time * 0.001) * 100 (JS detectBeat) 500 ± 100 ms Beat interval. Lower the center to 350 for ~170 BPM; raise to 800 for a slow pace (~75 BPM). Replace the whole expression with a constant for a steady tempo.
for (let i = 0; i < 5; i++) (JS pool init) 5 rings Pool size. Raise to 8–10 for more simultaneous overlap; drop to 3 for a minimal look.

FAQ

The code uses getElementById(‘pulseRingContainer’) and a rings array closed in the IIFE — only one instance is handled by default. For multiple instances, extract the logic into a factory function that takes an element, creates its own ring pool, and shares the requestAnimationFrame loop: beat detection via lastBeat remains shared, keeping all instances synchronized to the same tempo.
Pure JavaScript. animatePulseRing writes style.opacity and style.transform directly onto each ring every requestAnimationFrame tick — no @keyframes, no CSS animation property, no transition. This gives full control over timing and easing, at the cost of a hard dependency on the rAF loop.
Replace detectBeat with a function that analyzes energy from an AudioContext stream. Connect your source to an AnalyserNode, compute the RMS of time-domain data each frame, and trigger a beat when RMS exceeds a threshold. The lastBeat variable can remain to prevent triggers that are too close together.