25 CSS Spinners 05 / 25
Heartbeat Pulse Loader
A vivid red core pulses with a cardiac double-beat rhythm while three concentric ripple rings expand outward and fade, mimicking a real heartbeat waveform.
This is a full-page demo — interact inside the frame above, or open it in the playground for the full-screen experience.
The code
<div class="sp-05">
<div class="sp-05__stack">
<div class="sp-05__ring"></div>
<div class="sp-05__ring"></div>
<div class="sp-05__ring"></div>
<div class="sp-05__core"></div>
</div>
</div> <div class="sp-05">
<div class="sp-05__stack">
<div class="sp-05__ring"></div>
<div class="sp-05__ring"></div>
<div class="sp-05__ring"></div>
<div class="sp-05__core"></div>
</div>
</div>.sp-05,.sp-05 *,.sp-05 *::before,.sp-05 *::after{box-sizing:border-box;margin:0;padding:0}
.sp-05{
--bg:#0f0a0a;
--c:#ff1744;
--glow:rgba(255,23,68,0.5);
display:flex;
align-items:center;
justify-content:center;
min-height:100vh;
background:var(--bg);
}
.sp-05__stack{
position:relative;
width:72px;
height:72px;
}
.sp-05__ring{
position:absolute;
border-radius:50%;
border:2px solid var(--c);
opacity:0;
animation:sp-05-ripple 1.8s ease-out infinite;
}
.sp-05__ring:nth-child(1){
inset:0;
animation-delay:0s;
}
.sp-05__ring:nth-child(2){
inset:0;
animation-delay:0.6s;
}
.sp-05__ring:nth-child(3){
inset:0;
animation-delay:1.2s;
}
.sp-05__core{
position:absolute;
inset:22px;
border-radius:50%;
background:var(--c);
box-shadow:0 0 16px var(--glow),0 0 32px var(--glow);
animation:sp-05-beat 1.8s ease-in-out infinite;
}
@keyframes sp-05-ripple{
0%{transform:scale(0.3);opacity:0.9}
100%{transform:scale(1.6);opacity:0}
}
@keyframes sp-05-beat{
0%,100%{transform:scale(1)}
15%{transform:scale(1.25)}
30%{transform:scale(0.95)}
45%{transform:scale(1.1)}
}
@media (prefers-reduced-motion: reduce){
.sp-05__ring,.sp-05__core{animation:none;opacity:1}
} .sp-05,.sp-05 *,.sp-05 *::before,.sp-05 *::after{box-sizing:border-box;margin:0;padding:0}
.sp-05{
--bg:#0f0a0a;
--c:#ff1744;
--glow:rgba(255,23,68,0.5);
display:flex;
align-items:center;
justify-content:center;
min-height:100vh;
background:var(--bg);
}
.sp-05__stack{
position:relative;
width:72px;
height:72px;
}
.sp-05__ring{
position:absolute;
border-radius:50%;
border:2px solid var(--c);
opacity:0;
animation:sp-05-ripple 1.8s ease-out infinite;
}
.sp-05__ring:nth-child(1){
inset:0;
animation-delay:0s;
}
.sp-05__ring:nth-child(2){
inset:0;
animation-delay:0.6s;
}
.sp-05__ring:nth-child(3){
inset:0;
animation-delay:1.2s;
}
.sp-05__core{
position:absolute;
inset:22px;
border-radius:50%;
background:var(--c);
box-shadow:0 0 16px var(--glow),0 0 32px var(--glow);
animation:sp-05-beat 1.8s ease-in-out infinite;
}
@keyframes sp-05-ripple{
0%{transform:scale(0.3);opacity:0.9}
100%{transform:scale(1.6);opacity:0}
}
@keyframes sp-05-beat{
0%,100%{transform:scale(1)}
15%{transform:scale(1.25)}
30%{transform:scale(0.95)}
45%{transform:scale(1.1)}
}
@media (prefers-reduced-motion: reduce){
.sp-05__ring,.sp-05__core{animation:none;opacity:1}
}How this works
The core element uses a multi-step sp-05-beat keyframe that replicates a cardiac double-peak: a first strong beat at 15% (scale 1.25), a rebound at 30% (scale 0.95), a secondary beat at 45% (scale 1.1), then return to baseline — matching the QRS complex shape of an electrocardiogram.
Three staggered ripple rings are all position:absolute;inset:0, starting at scale(0.3) with full opacity and animating to scale(1.6) at zero opacity. Each ring starts 0.6s later than the previous, so at any moment one ring is expanding from centre, one is mid-ripple, and one is fading at the periphery.
Customize
- Change the heart rate by adjusting the animation duration from
1.8s— shorter durations (e.g.0.9s) simulate elevated heart rate. - Edit
--cto any colour for brand adaptation; the glow and rings inherit the variable automatically. - Add a fourth ripple ring at
animation-delay:1.8sto fill the gap at the end of the cycle. - Scale the core element size via
inset:22px— decrease to make the dot smaller relative to the ripple area. - Reduce scale values in
sp-05-beat(e.g. max1.1instead of1.25) for a subtler, restful pulse more suitable as a passive loading indicator.
Watch out for
- The
transform:scaleon the core alongsidebox-shadowcan cause sub-pixel blur on retina displays at the compressed0.95scale step — acceptable visually but worth noting. - Three simultaneous infinite ring animations on the same stacking layer can create compositing overhead; use
will-change:transform,opacityon the rings if profiling shows jank. - The ripple rings use
inset:0so they match the wrapper size exactly — if the wrapper has padding, the rings will appear smaller than intended.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 60+ | 12+ | 60+ | 60+ |
Fully compositor-based; no vendor prefixes required in modern browsers.