Heartbeat
CSSA keep-alive pulse travels the length of a wire at a fixed interval, never varying.
- Tier
- CSS
- Dependencies
- None
- Duration
- 4 s, loops
- Categories
- Loaders and spinners
- Use cases
- Loading and waiting, Dashboards and reporting
- Industries
- SaaS and software, Security and identity
- Style
- Minimal line · light stroke
- Set
- signal
- Tags
- loopminimaluistrokepulse
Companion assets
The same artwork, not animated. Derived from the entry’s own resting state, so it matches exactly.
Code
animation.svg
<svg xmlns="http://www.w3.org/2000/svg" aria-labelledby="signal-heartbeat-title" class="pulsar pulsar-signal-heartbeat" data-pulsar="signal-heartbeat" role="img" viewBox="0 0 120 40"><title id="signal-heartbeat-title">Heartbeat</title><circle id="signal-heartbeat-node-a" cx="15" cy="20" r="4" fill="none" stroke="currentColor" stroke-width="2"></circle><circle id="signal-heartbeat-node-b" cx="105" cy="20" r="4" fill="none" stroke="currentColor" stroke-width="2"></circle><path id="signal-heartbeat-wire" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 20h35l3-8 3 16 3-8h46" opacity=".2"></path><path id="signal-heartbeat-pulse" fill="none" stroke="var(--pulsar-accent, currentColor)" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 20h35l3-8 3 16 3-8h46" class="signal-heartbeat-pulse-element" pathLength="114"></path><circle id="signal-heartbeat-ripple-a" cx="15" cy="20" r="4" fill="none" stroke="currentColor" stroke-width="1.5" class="signal-heartbeat-ripple signal-heartbeat-ripple-a"></circle><circle id="signal-heartbeat-ripple-b" cx="105" cy="20" r="4" fill="none" stroke="var(--pulsar-accent, currentColor)" stroke-width="1.5" class="signal-heartbeat-ripple signal-heartbeat-ripple-b"></circle></svg>
animation.css
/* A keep-alive pulse travels the wire at a regular fixed interval. */
.pulsar-signal-heartbeat .signal-heartbeat-ripple {
opacity: 0;
transform-box: fill-box;
transform-origin: center;
animation-duration: 4000ms;
animation-iteration-count: infinite;
}
.pulsar-signal-heartbeat .signal-heartbeat-ripple-a {
animation-name: signal-heartbeat-ripple-a-anim;
}
.pulsar-signal-heartbeat .signal-heartbeat-ripple-b {
animation-name: signal-heartbeat-ripple-b-anim;
}
.pulsar-signal-heartbeat .signal-heartbeat-pulse-element {
stroke-dasharray: 20 120;
stroke-dashoffset: 20;
opacity: 0;
animation: signal-heartbeat-pulse-anim 4000ms infinite;
}
@keyframes signal-heartbeat-ripple-a-anim {
0% {
transform: scale(1);
opacity: 1;
animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1); /* settle */
}
15% {
transform: scale(1.8);
opacity: 0;
}
15.01%,
100% {
transform: scale(1);
opacity: 0;
}
}
@keyframes signal-heartbeat-ripple-b-anim {
0%,
69.99% {
transform: scale(1);
opacity: 0;
}
70% {
transform: scale(1);
opacity: 1;
animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1); /* settle */
}
85% {
transform: scale(1.8);
opacity: 0;
}
85.01%,
100% {
transform: scale(1);
opacity: 0;
}
}
@keyframes signal-heartbeat-pulse-anim {
0% {
stroke-dashoffset: 20;
opacity: 1;
animation-timing-function: cubic-bezier(0.37, 0, 0.63, 1); /* travel */
}
70% {
stroke-dashoffset: -115;
opacity: 1;
animation-timing-function: cubic-bezier(0.4, 0, 0.6, 1); /* fade */
}
78% {
stroke-dashoffset: -115;
opacity: 0;
}
78.01%,
100% {
stroke-dashoffset: 20;
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.pulsar-signal-heartbeat .signal-heartbeat-ripple-a,
.pulsar-signal-heartbeat .signal-heartbeat-ripple-b {
animation: none;
opacity: 0;
transform: scale(1);
}
.pulsar-signal-heartbeat .signal-heartbeat-pulse-element {
animation: none;
opacity: 1;
stroke-dashoffset: -40;
}
.pulsar-signal-heartbeat #signal-heartbeat-wire {
opacity: 0.3;
}
}