Heartbeat Pulse
The text gives a double beat, a small one then a bigger one, rests, and beats again, like a pulse. Set the beats per minute, and an optional colour it flushes on each beat.
♥ Live now
How to use it
- Paste the snippet just before
</head>. - Add
data-heartbeat-pulseto a heading, badge or button.
Options
Add any of these attributes to the same element.
data-bpm | Beats per minute. Default 72. |
data-scale | Size at the bigger beat. Default 1.15. |
data-color | A colour the text flushes on each beat. |
It only beats while on screen. Great on “Live” badges and like counts.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Heartbeat Pulse: the text gives a double beat, a small one then a bigger
// one, rests, and beats again, like a pulse. Keeps going while it's on
// screen.
// Usage: <h1 data-heartbeat-pulse>Live now</h1>
// Optional: data-bpm (beats per minute; default 72), data-scale (size at
// the bigger beat; default 1.15), data-color (a colour the text flushes on
// each beat).
var DEFAULTS = { bpm: 72, scale: 1.15 };
var css = '.hbp-box{will-change:transform}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
// What moves: buttons, badges and other boxes move as a whole. A block
// heading's text goes in an inline box, so it moves around its own centre
// rather than the middle of the full-width block.
function build(el) {
var display = getComputedStyle(el).display;
if (display === 'inline') el.style.display = 'inline-block';
if (display !== 'block') { el.classList.add('hbp-box'); return el; }
var box = document.createElement('span');
box.className = 'hbp-box';
box.style.display = 'inline-block';
while (el.firstChild) box.appendChild(el.firstChild);
el.appendChild(box);
return box;
}
function start(el, box) {
var d = el.dataset;
var big = +d.scale || DEFAULTS.scale;
var small = 1 + (big - 1) * 0.55;
// The beat itself takes 700 ms; the rest of each period is the pause.
var period = 60000 / (+d.bpm || DEFAULTS.bpm);
var props = {
// Lub, a quick release, dub, then a slow ease back to rest.
scale: [
{ to: small, duration: 110, ease: 'out(3)' },
{ to: 1, duration: 130, ease: 'in(2)' },
{ to: big, duration: 120, ease: 'out(3)' },
{ to: 1, duration: 340, ease: 'out(2)' }
],
loop: true,
loopDelay: Math.max(0, period - 700)
};
if (d.color) {
var base = getComputedStyle(el).color;
// The colour flushes in on the bigger beat and drains away with it.
props.color = [
{ to: base, duration: 240 },
{ to: d.color, duration: 120, ease: 'out(3)' },
{ to: base, duration: 340, ease: 'out(2)' }
];
}
return anime.animate(box, props);
}
function init() {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
document.querySelectorAll('[data-heartbeat-pulse]').forEach(function (el) {
if (el._hbp) return;
var box = (el._hbp = build(el));
var anim = null;
// Only beat while visible, to save battery.
new IntersectionObserver(function (entries) {
if (!entries[0].isIntersecting) { if (anim) anim.pause(); return; }
if (anim) anim.play();
else anim = start(el, box);
}, { threshold: 0.3 }).observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><h1 data-heartbeat-pulse>Live now</h1>
<!-- slower, flushing pink: -->
<span data-heartbeat-pulse data-bpm="60" data-color="#ec4899">♥ 128</span>