ECG Heartbeat
Live BPM with a continuously scrolling waveform. Scroll speed equals beats per minute literally — change the duration and you change the rate. The badge and the data are the same thing.
ECG Heartbeat the 13th of 30 designs in the 30 CSS Badges collection. The design pairs CSS styling with a small amount of JavaScript for interactivity. Copy the HTML, CSS and JavaScript panels below into your project — the JS is self-contained, has zero dependencies, and is safe to drop into any framework (React, Vue, Svelte, plain HTML). The design honours prefers-reduced-motion and uses real semantic markup, so it ships accessibility-ready out of the box.
Live preview
The code
<div class="ecg-stage">
<div class="ecg-card">
<div class="ecg-header">
<div class="ecg-system">SRV-PROD-01 · Heartbeat</div>
<div class="ecg-status">Normal</div>
</div>
<div class="ecg-viewport">
<svg class="ecg-svg" width="880" height="68" viewBox="0 0 880 68" style="animation-duration: 0.833s" aria-hidden="true">
<polyline
fill="none"
stroke="#00e5a0"
stroke-width="2"
stroke-linejoin="round"
stroke-linecap="round"
opacity="0.9"
points="
0,48 58,48
64,42 70,38 76,42 82,48
132,48 136,53 139,10 143,60 147,48
170,48 185,40 200,36 215,40 230,48
300,48
358,48
364,42 370,38 376,42 382,48
432,48 436,53 439,10 443,60 447,48
470,48 485,40 500,36 515,40 530,48
600,48
658,48
664,42 670,38 676,42 682,48
732,48 736,53 739,10 743,60 747,48
770,48 785,40 800,36 815,40 830,48
880,48
"/>
<line x1="0" y1="48" x2="880" y2="48" stroke="#00e5a0" stroke-width="0.3" opacity="0.2"/>
</svg>
</div>
<div class="ecg-metrics">
<div class="ecg-bpm">
<div class="ecg-bpm-val" id="ecg-bpm">72</div>
<div class="ecg-bpm-unit">BPM</div>
</div>
<div class="ecg-secondary">
<div class="ecg-sec-val">P–R 160ms</div>
<div class="ecg-sec-label">Interval</div>
</div>
<div class="ecg-secondary">
<div class="ecg-sec-val">QRS 88ms</div>
<div class="ecg-sec-label">Duration</div>
</div>
</div>
</div>
</div> .ecg-stage {
background: #050c12;
padding: 48px 32px;
display: flex;
flex-direction: column;
gap: 20px;
justify-content: center;
align-items: center;
min-height: 360px;
}
.ecg-card {
background: #0a1520;
border: 1px solid #0f2030;
border-radius: 8px;
padding: 20px 24px;
width: 100%;
max-width: 440px;
}
.ecg-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
}
.ecg-system {
font-family: ui-monospace, "JetBrains Mono", monospace;
font-size: 10px;
letter-spacing: 0.24em;
text-transform: uppercase;
color: #00e5a0;
opacity: 0.7;
}
.ecg-status {
font-family: system-ui, "Bricolage Grotesque", sans-serif;
font-size: 10px;
letter-spacing: 0.22em;
text-transform: uppercase;
color: #00e5a0;
padding: 3px 10px;
border: 1px solid rgba(0,229,160,0.3);
border-radius: 20px;
animation: ecg-glow 3s ease-in-out infinite;
}
@keyframes ecg-glow {
0%, 100% { box-shadow: 0 0 0 rgba(0,229,160,0.3); }
50% { box-shadow: 0 0 8px rgba(0,229,160,0.3); }
}
.ecg-viewport {
width: 100%;
height: 68px;
overflow: hidden;
position: relative;
margin-bottom: 14px;
}
.ecg-svg {
display: block;
animation: ecg-scroll 0.833s linear infinite;
}
@keyframes ecg-scroll {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
@media (prefers-reduced-motion: reduce) {
.ecg-svg, .ecg-status { animation: none; }
}
.ecg-metrics {
display: flex;
align-items: flex-end;
gap: 24px;
padding-top: 12px;
border-top: 1px solid #0f2030;
}
.ecg-bpm {
display: flex;
align-items: baseline;
gap: 5px;
}
.ecg-bpm-val {
font-family: ui-monospace, "JetBrains Mono", monospace;
font-size: 42px;
font-weight: 700;
color: #00e5a0;
line-height: 0.9;
letter-spacing: -0.02em;
}
.ecg-bpm-unit {
font-family: system-ui, "Bricolage Grotesque", sans-serif;
font-size: 12px;
letter-spacing: 0.12em;
color: rgba(0,229,160,0.6);
text-transform: uppercase;
}
.ecg-secondary {
display: flex;
flex-direction: column;
gap: 4px;
margin-left: auto;
text-align: right;
}
.ecg-sec-val {
font-family: ui-monospace, "JetBrains Mono", monospace;
font-size: 14px;
color: rgba(0,229,160,0.8);
letter-spacing: 0.04em;
}
.ecg-sec-label {
font-family: system-ui, "Bricolage Grotesque", sans-serif;
font-size: 9px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: rgba(0,229,160,0.4);
} // BPM drifts slightly around 72 so the badge feels alive. Animation
// duration is tied to BPM literally — 60 / bpm seconds per cycle.
(function () {
var bpmEl = document.getElementById('ecg-bpm');
var ecgSvg = document.querySelector('.ecg-svg');
var bpm = 72;
setInterval(function () {
bpm += (Math.random() - 0.5) * 2;
bpm = Math.max(68, Math.min(78, bpm));
var rounded = Math.round(bpm);
bpmEl.textContent = rounded;
ecgSvg.style.animationDuration = (60 / bpm).toFixed(3) + 's';
}, 4000);
})();