Loaders✨ Premium

Heartbeat

A 40 px red circle pulsing in a double beat — first peak at 140 ms, second at 420 ms, ×1.3 at each beat, 300 ms rest — 60 BPM, pure CSS, zero JavaScript.

CSSPulse

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

Health app — "Heart Rate" card awaiting sync — Heartbeat example 1

① Health app — "Heart Rate" card awaiting sync

WhenA connected health dashboard: the watch is syncing, the card is waiting for the value.
WhyThe shape (red circle) and the double rhythm (lub-dub at 60 BPM) echo the universal visual code of the heart without needing a ❤️ icon. This is one of the few indicators where color and rhythm carry meaning, not just decoration.
SettingsSize reduced to 24 × 24 px to fit inline with data; color unchanged; add role="status" and aria-label="Syncing".
Service monitoring — the health dot beats as long as the server answers — Heartbeat example 2

② Service monitoring — the health dot beats as long as the server answers

WhenA monitoring console: every service sends a sign of life at a fixed interval (here every 5 s). As long as the server answers, the dot beats next to its name and latency; when the signal stops, so does the dot.
Why"Heartbeat" is the exact monitoring term for this liveness signal: the effect carries the name of the thing it represents. A spinner would say "waiting for a result", a progress bar "a task is advancing"; here nothing is loading, the server is simply alive — and only a two-beat rhythm followed by a 300 ms rest reads as a pulse rather than a blink. At 40 px (52 px at the peak), the two contractions can be told apart, which a 10 px dot never allows.
SettingsOriginal size and rhythm kept (40 × 40 px, scale(1.3) → 52 px at the peak, animation-duration: 1s = 60 BPM) so both beats stay readable; color switched to green (background: #22c55e), the monitoring code for healthy. When the signal is lost, drop the animation (animation: none) and revert to the original red #ef4444. The row carries role="status"; the text "Alive · HTTP 200" backs up what color and motion convey.
Live dot — video stream connecting indicator — Heartbeat example 3

③ Live dot — video stream connecting indicator

WhenA streaming or video-conferencing interface: the stream is not yet established, the badge pulses while waiting for the connection.
WhyThe cardiac pulse is the canonical visual signal for "alive, connecting". The double beat avoids confusion with a plain spinner and echoes the red indicator of live TV. Once the stream is active, swap the animation class for a slow pulse (2 s) or a solid color.
SettingsSize at 10 × 10 px next to the text "LIVE"; animation-duration: 1.4s for a slower rate (43 BPM) during connection; red unchanged.

How it works

A single div.loader-heartbeat: 40 × 40 px, background: #ef4444 (red-500), border-radius: 50% for a perfect circle. The animation property is fully resolved in the sold CSS: 1s ease-in-out 0s infinite normal none running heartbeat — 1 s duration, no delay, infinite iterations. No children, no pseudo-elements, no JavaScript.

The @keyframes heartbeat animation has six steps describing a double beat (lub-dub): scale(1) at 0% → scale(1.3) at 14% (first beat peak at 140 ms) → scale(1) at 28% → scale(1.3) at 42% (second beat peak at 420 ms) → scale(1) at 70% → scale(1) at 100%. The rest pause between cycles lasts 300 ms (from 700 ms to 1,000 ms). At maximum scale, the circle measures 52 × 52 px. The ease-in-out easing softens each rise and fall. The second beat has a longer descent (280 ms vs. 140 ms for the first), matching a real cardiac rhythm.

Accessibility

  • prefers-reduced-motion not handled: verified under Playwright, the animation keeps running (animationPlayState: running) when the preference is enabled. No @media (prefers-reduced-motion) rule exists in the sold CSS. Add after the .loader-heartbeat block: @media (prefers-reduced-motion: reduce) { .loader-heartbeat { animation: none; } }. The red circle stays visible — a readable static indicator with no motion.
  • As a loading indicator, this element has no semantic role in the sold code: there is no role="status", no aria-busy, no aria-live, and no alternative text. Screen readers ignore it. To announce the loading state, wrap the circle in a <span role="status" aria-label="Loading"> or add visually-hidden text.
  • Color alone: the effect relies solely on the red color and motion to signal activity. Make sure the context provides a text label or complementary icon (e.g. "Sending…") — especially if you change the color. On a dark background (#0a0a0f), #ef4444 measures 3.7:1, above the 3:1 threshold for graphic elements.
  • The sold code includes a .demo-preview wrapper with background: #0a0a0f: this container is used only for the catalog demo. You only need .loader-heartbeat and the @keyframes heartbeat rule.

Browser compatibility

Pure CSS: transform: scale(), border-radius, animation. Zero JavaScript, zero dependencies.

Chrome 36+✓ Full
Firefox 16+✓ Full
Safari 9+✓ Full
Edge 12+✓ Full
Mobile iOS✓ Full
Android Chrome✓ Full

Without CSS animations (very old browsers), the red circle remains visible and centered — a silent static indicator. border-radius: 50% is supported from Chrome 4 / Firefox 4 / Safari 5.

The code

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

index.html — structure
<div class="loader-heartbeat"></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
Duration / BPM (CSS — animation-duration: 1s) 1 s (60 BPM) Change the value in the .loader-heartbeat rule. Keyframes stay proportional: 14% = first peak, 42% = second peak, 70% = end of second beat. 0.8s → 75 BPM (moderate effort); 1.4s → 43 BPM (rest, long wait); 0.5s → 120 BPM (alert).
Beat amplitude (CSS — scale(1.3)) ×1.3 (40 px → 52 px at each peak) At keyframes 14% and 42%, replace 1.3. Values below 1.1 make the pulse nearly invisible; above 1.5 the circle may overflow in tight layouts — check overflow: hidden on the parent.
Size (CSS — width/height: 40px) 40 × 40 px Resize directly. 18–20 px for a button; 60–80 px for a full-screen loading view. Consider lowering scale to 1.2 at small sizes to limit visual shift.
Color (CSS — background: #ef4444) #ef4444 (red-500) Replace the hex value. Red encodes vitality in this context; a neutral color (#6366f1) works for a generic loader. Check 3:1 contrast against your background.
Beat timing (CSS — keyframes 14%, 28%, 42%, 70%) 14%→28% (1st) · 42%→70% (2nd) · rest 70%–100% Move 14% and 42% closer (e.g. 10% / 35%) for a hurried rhythm; further apart (20% / 55%) for a slow, deep beat. Shortening the rest (70%→100% → 60%→100%) speeds up the cycle without changing the duration.

FAQ

The name refers to the rhythm, not the shape. The code draws a red circle (border-radius: 50%) pulsing in a double beat (lub-dub): first peak at 140 ms, second at 420 ms, 300 ms rest. For an actual heart, replace the div with a ❤ SVG or emoji in a span: the @keyframes heartbeat rule and animation shorthand apply as-is to any shape.
Bouncing Dots uses three indigo spans that appear and disappear (scale 0 → 1 → 0) in a regular wave over 1.4 s. Heartbeat has a single red element that grows and shrinks with an asymmetric rhythm (two close beats then a 300 ms pause), no children, no JavaScript. The use case differs: dots suit any generic loading context; the cardiac pulse specifically signals activity in a medical, vital, or emotional register.
The sold code contains no @media (prefers-reduced-motion) rule (verified). Add after the .loader-heartbeat block: @media (prefers-reduced-motion: reduce) { .loader-heartbeat { animation: none; } }. The red circle stays visible — a readable static indicator with no motion. For a gentler fallback, substitute a slow fade: animation: 2s ease-in-out infinite alternate heartbeat-fade with @keyframes heartbeat-fade { from { opacity: .4 } to { opacity: 1 } }.