Loaders✨ Premium

Pulse Ring

Two 50 px rings expand from 25 to 75 px in 2 s (ease-out), fade from opacity 1 to 0 and loop — ::after delayed by exactly 1 s. Pure CSS, no script.

CSSRadar

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. Loaders has 29 effects, including 4 free. Explore the category →

3 usage examples

Searching for a nearby device — Bluetooth settings — Pulse Ring example 1

① Searching for a nearby device — Bluetooth settings

WhenThe user opens the Bluetooth settings (or a local-sharing screen) to add a device; the double ring pulses at the center of the screen, like a radar sweep, as long as no device answers.
WhyTwo rings born at the center, expanding and fading, are literally the picture of a radio signal leaving the device and losing range: the loader's shape is the scanning metaphor, no icon has to explain it. The half-period offset yields one ping per second — a radar's cadence — and ease-out makes each wave burst out before dying away. A spinner would say 'computing' and a bar would promise an end; here the wait depends on a third-party device and has no known term. At an 80 px container, the 40-to-120 px expansion reads as a sweep, not as a blinking dot.
SettingsContainer raised to 80 × 80 px in .loader-pulse (rings from 40 to 120 px), color #3b82f6 (Bluetooth blue) on both borders; 3 px stroke, 2 s duration and 1 s ::after delay kept — one start per second. The Bluetooth glyph (30 px) is a sibling element absolutely centered on the container, not a child of .loader-pulse.
Contactless payment terminal — 'Tap your card' — Pulse Ring example 2

② Contactless payment terminal — 'Tap your card'

WhenThe amount is displayed and the terminal waits for a card, phone or watch to enter the NFC field; the rings emanate from the contactless symbol until something is detected.
WhyThe contactless symbol is already made of concentric arcs: rings born under that glyph and expanding extend it into motion — they draw the field the card must reach and point to its entry spot. Above all, this loader does not say 'the terminal is working' (a spinner would invite you to wait) but 'the terminal is listening, your move': a steady beat, with no rotation and no promised end, which stays legible on a terminal's small screen held at arm's length thanks to the 120 px each wave reaches.
SettingsContainer 80 × 80 px (rings from 40 to 120 px), #fff borders with the 3 px stroke kept so they stay crisp on the terminal's dark screen; 1.6 s duration, ::after delay at 0.8 s (half-period) — one wave every 0.8 s. The contactless glyph (34 px) is an absolutely centered sibling of the container: the first ring (40 px) appears right at its edge.
Live data refresh — dashboard card — Pulse Ring example 3

③ Live data refresh — dashboard card

WhenA dashboard card reloads its data in the background; the indicator temporarily replaces the content or overlays it.
WhyIndeterminate pulsing matches a short wait of unknown length, common in periodic API calls. The absence of JavaScript lightens dashboards with many components.
SettingsColor #34d399 (green — 'fresh data' convention), container 40 × 40 px, border-width: 2px, duration 1.8 s, ::after delay at 0.9 s.

How it works

The effect relies on two pseudo-elements (::before and ::after) of the .loader-pulse container (50 × 50 px, position: relative). Each is absolutely positioned at inset: 0 — it occupies the same 50 × 50 px square as the container — with border: 3px solid #6366f1 and border-radius: 50%, making it a ring. No child HTML elements are needed; no JavaScript is loaded.

The @keyframes pulse animation moves transform from scale(.5) to scale(1.5) and opacity from 1 to 0, over 2 s ease-out, looping infinitely. The ring's visible diameter goes from 25 px (50 × 0.5) to 75 px (50 × 1.5); since scale() also affects the stroke, the apparent border width goes from 1.5 px at the start to 4.5 px at the end — the ring thickens slightly as it expands. The ease-out curve produces a fast initial expansion that slows toward the end: the ring bursts from the center, then floats before fading. ::after has animation-delay: 1s, exactly half the period: at any moment, one ring is born while the other fades — one new start per second, two alternating cycles without interruption.

The code contains no var(--…) references (verified: zero occurrences) and no scripts. The inset property requires Chrome 87+, Firefox 87+, or Safari 14.1+. The name @keyframes pulse is not prefixed: if the buyer's page already defines an animation with that name, the ring will adopt the page's animation — rename it if this collision is possible. The sold HTML includes the site's div.demo-preview; the buyer only needs <div class="loader-pulse"></div> and the CSS without that wrapper.

Accessibility

  • prefers-reduced-motion not handled — measured with Playwright: animationPlayState: "running" under emulated prefers-reduced-motion: reduce (Chromium). Both rings animate regardless of the OS preference. Fix: add @media (prefers-reduced-motion: reduce) { .loader-pulse::before, .loader-pulse::after { animation: none; } }.
  • No accessibility attributes: no role="status", no aria-busy, no aria-live, no alternative text. Screen readers do not announce the loading state. For a functional indicator, add role="status" to .loader-pulse and a visually-hidden <span class="sr-only">Loading…</span> inside it.
  • The animation is purely visual and conveys no information through color alone: the ring is either visible or gone. If the loading state must be communicated, a text message must accompany the component — the effect alone is not sufficient.
  • The name @keyframes pulse is not prefixed: a naming collision on the host page may cause the ring to adopt a foreign animation and the visual behavior is no longer guaranteed. Rename to loader-pulse-anim or similar if multiple 'pulse' animations coexist.

Browser compatibility

Pure CSS: requires @keyframes, transform: scale(), opacity, inset, and border-radius: 50%. No JS dependencies.

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

Without inset (Chrome < 87, Safari < 14.1): pseudo-elements no longer position inside the container — replace with top: 0; right: 0; bottom: 0; left: 0 for broader support. Without CSS animations, both pseudo-elements remain frozen at scale(.5) — two small static arcs, no error, page intact.

The code

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

index.html — structure
<div class="loader-pulse"></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
Container size (CSS — width / height of .loader-pulse) 50 × 50 px Starting diameter (× 0.5) and ending diameter (× 1.5) of the rings. 24 px = button indicator; 80–100 px = full-screen centering.
Color (CSS — border: 3px solid #6366f1) #6366f1 (indigo) Color of both rings. Changing the hex value is enough; rgba lets you adjust the initial opacity independently of the animation.
Stroke width (CSS — border: 3px solid …) 3 px Nominal stroke width; scale() multiplies it: visually 1.5 px at the start, 4.5 px at the end. Raise to 5 px for more prominent rings on large formats.
Cycle duration (CSS — animation: 2s …) 2 s Duration of one full scale(0.5) → scale(1.5) pass. 1 s = fast pulse; 3–4 s = slow breathing. If you change this value, update the second ring's delay to half of the new duration.
Second ring offset (CSS — animation-delay: 1s on ::after) 1 s (half-period) Phase shift between the two rings. At half-period, cycles alternate perfectly — one start per second. 0.5 s (quarter-period): rings overlap more, creating a 'double beat' effect.
Starting scale (CSS — scale(.5) in @keyframes pulse) 0.5 — ring at 25 px diameter on a 50 px container scale(0): the ring starts from a geometric point (shock-wave effect). scale(0.8): short, subtle expansion, already close to full size.
Timing curve (CSS — ease-out) ease-out Fast initial expansion, slowing at the end. linear: steady mechanical expansion. ease-in-out: soft start and end, more symmetric motion — closer to a sonar ping.

FAQ

fx-0518 is an audio visualizer: it generates 5 rings in JavaScript via requestAnimationFrame, computes a simulated beat interval (500 + Math.sin(time × 0.001) × 100 ms, yielding 1.7 to 2.5 pulses per second), applies linear interpolation on a 120 px container, and shares its rAF loop with other visualizers in the same IIFE. fx-0423 is a loading indicator: 2 CSS pseudo-elements, no JavaScript, fixed 2 s ease-out duration, exact half-period phase offset. The two effects have no functional overlap.
At scale(0), the ring would emerge from a geometric point — a 'shockwave' or 'water drop' effect. At scale(0.5), it starts at 25 px in diameter: the eye sees a ring expanding and fading, not a circle being born. This makes the pulse softer and more readable at small sizes (24 px container). For the shockwave look, replace scale(.5) with scale(0) — the stroke will then be 0 px at the start and 4.5 px at the end.
Declare a CSS variable on the container: .loader-pulse { --pr-dur: 2s; } and replace the values with animation: var(--pr-dur) ease-out … and animation-delay: calc(var(--pr-dur) / 2). Both rings stay automatically at the half-period whatever the duration, and multiple instances can coexist at different speeds by overriding --pr-dur on each container.