Clap Counter
A clap button with a running total. Every click adds a clap: the hands pop, a ring and a spray of little rays fly out, the number ticks up and a “+1” bubble rises above. Keep clicking and the bubble counts your claps, then floats away once you stop.
How to use it
- Paste the snippet just before
</head>. - Add
data-clap-counterto a button, with the current total indata-count. Its text becomes its name for screen readers. - Save each clap when it fires its
clapevent.
Options
Add any of these attributes to the same element.
data-count | The starting total. Default 0. |
data-max | Most claps one visitor can give; after that it just shakes. Default 50. |
data-icon | The icon. Default 👏. |
data-color | Colour of the burst and bubble. Default #6a0dad. |
data-autoplay | It claps itself in little runs while in view, until someone presses it, without firing the event. Handy for demos. |
The event’s detail has count (the new total) and claps (how many this visitor has given). Leave a little room above the button for the bubble. With reduced motion the number just goes up.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Clap Counter: a clap button with a running total. Every click adds a
// clap: the hands pop, a ring and a burst of little rays fly out, and a
// "+1" bubble rises above. Keep clicking and the bubble counts up your
// claps ("+2", "+3"…), then floats away once you stop.
// Usage: <button data-clap-counter data-count="128">Clap</button>
// (the text is only used as its accessible name)
// Optional: data-count (starting total, default 0), data-max (most claps
// one visitor can give, default 50), data-icon (default 👏), data-color
// (colour of the burst and bubble, default #6a0dad), data-autoplay (it
// claps itself in little runs while in view, until someone presses it,
// without firing the event; handy for demos).
// Events: fires "clap" on the button with event.detail.count (the total)
// and event.detail.claps (how many this visitor has given).
var DEFAULTS = { count: 0, max: 50, icon: '👏', color: '#6a0dad', every: 2600 };
var RAYS = 7;
var css =
':where(.clp){font:inherit;font-weight:700;padding:.4em 1.05em .4em .4em;border-radius:999px;border:1px solid #e4e1ea;background:#fff;color:#374151;' +
'box-shadow:0 3px 12px rgba(15,15,30,.07)}' +
'.clp{position:relative;display:inline-flex;align-items:center;gap:.55em;cursor:pointer;user-select:none;-webkit-user-select:none;' +
'touch-action:manipulation;-webkit-tap-highlight-color:transparent;transition:border-color .3s}' +
'.clp.clp-on{border-color:color-mix(in srgb,var(--clp-c) 35%,#e4e1ea)}' +
'.clp-icon{position:relative;display:grid;place-items:center;width:2.1em;height:2.1em;border-radius:50%;' +
'border:1.5px solid #e4e1ea;transition:background-color .3s,border-color .3s}' +
'.clp-on .clp-icon{border-color:var(--clp-c);background:color-mix(in srgb,var(--clp-c) 9%,transparent)}' +
'.clp-hands{display:block;font-size:1.15em;line-height:1}' +
'.clp-num{display:inline-block;min-width:2ch;font-variant-numeric:tabular-nums;transition:color .3s}' +
'.clp-on .clp-num{color:var(--clp-c)}' +
'.clp-ring{position:absolute;inset:-1.5px;border-radius:50%;border:2px solid var(--clp-c);opacity:0;pointer-events:none}' +
'.clp-ray{position:absolute;left:50%;top:50%;width:0;height:0;pointer-events:none}' +
'.clp-ray>span{position:absolute;left:-1.5px;bottom:0;width:3px;height:.45em;border-radius:3px;background:var(--clp-c);opacity:0}' +
'.clp-bub{position:absolute;left:50%;top:50%;display:grid;place-items:center;width:2.3em;height:2.3em;margin:-1.15em 0 0 -1.15em;' +
'border-radius:50%;background:var(--clp-c);color:#fff;font-size:.8em;font-weight:800;opacity:0;pointer-events:none;' +
'box-shadow:0 6px 16px color-mix(in srgb,var(--clp-c) 35%,transparent);z-index:2}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function setup(el) {
var d = el.dataset;
var total = parseInt(d.count, 10) || DEFAULTS.count;
var max = parseInt(d.max, 10) || DEFAULTS.max;
var label = el.textContent.trim() || 'Clap';
var still = reduced();
el.classList.add('clp');
el.style.setProperty('--clp-c', d.color || DEFAULTS.color);
el.textContent = '';
var icon = document.createElement('span');
icon.className = 'clp-icon';
icon.setAttribute('aria-hidden', 'true');
icon.innerHTML = '<span class="clp-ring"></span><span class="clp-hands"></span><span class="clp-bub"></span>';
var ring = icon.firstChild, hands = icon.children[1], bub = icon.lastChild;
hands.textContent = d.icon || DEFAULTS.icon;
var rays = [];
for (var i = 0; i < RAYS; i++) {
var ray = document.createElement('span');
ray.className = 'clp-ray';
ray.innerHTML = '<span></span>';
icon.appendChild(ray);
rays.push(ray);
}
var num = document.createElement('span');
num.className = 'clp-num';
num.setAttribute('aria-hidden', 'true');
el.appendChild(icon);
el.appendChild(num);
var mine = 0, fadeTimer = null, bubbleUp = false;
function render() {
num.textContent = total.toLocaleString();
el.setAttribute('aria-label', label + ', ' + total.toLocaleString() + (total === 1 ? ' clap' : ' claps'));
el.classList.toggle('clp-on', mine > 0);
}
render();
function burst() {
var turn = Math.random() * 360;
rays.forEach(function (ray, i) {
ray.style.transform = 'rotate(' + (turn + i * 360 / RAYS) + 'deg)';
anime.animate(ray.firstChild, {
y: ['-1.15em', '-1.85em'], scaleY: [1.2, 0.3], opacity: [1, 0],
duration: 520, ease: 'out(3)'
});
});
anime.animate(ring, { scale: [0.95, 1.45], opacity: [0.7, 0], duration: 560, ease: 'out(2)' });
}
function showBubble() {
bub.textContent = '+' + mine;
if (!bubbleUp) {
bubbleUp = true;
anime.animate(bub, { y: ['0em', '-2.9em'], scale: [0.3, 1], opacity: [0, 1], duration: 420, ease: 'outBack(1.8)' });
} else {
anime.animate(bub, { scale: [1.22, 1], duration: 260, ease: 'out(3)' });
}
clearTimeout(fadeTimer);
fadeTimer = setTimeout(function () {
bubbleUp = false;
anime.animate(bub, { y: '-4em', scale: 0.8, opacity: 0, duration: 420, ease: 'in(2)' });
}, 900);
}
function clap(fire) {
if (mine >= max) {
if (!still) {
anime.animate(icon, { x: [0, -4, 4, -3, 2, 0], duration: 360, ease: 'linear' });
showBubble();
}
return;
}
mine++;
total++;
render();
if (fire) el.dispatchEvent(new CustomEvent('clap', { bubbles: true, detail: { count: total, claps: mine } }));
if (still) return;
anime.animate(hands, { scale: [1, 0.78, 1.18, 1], rotate: [0, -10, 6, 0], duration: 380, ease: 'out(2)' });
anime.animate(num, { y: ['-55%', '0%'], opacity: [0, 1], duration: 260, ease: 'out(3)' });
burst();
showBubble();
}
var userTouched = false;
el.addEventListener('click', function (e) {
if (e.isTrusted) userTouched = true;
clap(true);
});
if (!still && d.autoplay !== undefined && d.autoplay !== 'false') {
var timer = null, runs = [3, 1, 5, 2], n = 0;
var run = function () {
if (userTouched) return;
if (mine > max - 6) mine = 0;
var k = runs[n++ % runs.length];
for (var i = 0; i < k; i++) {
setTimeout(function () { if (!userTouched) clap(false); }, i * 170);
}
timer = setTimeout(run, DEFAULTS.every);
};
new IntersectionObserver(function (entries) {
clearTimeout(timer);
if (entries[0].isIntersecting) timer = setTimeout(run, 700);
}).observe(el);
}
}
function init() {
document.querySelectorAll('[data-clap-counter]').forEach(function (el) {
if (el._clp) return;
el._clp = true;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><button id="clap" data-clap-counter data-count="128">Clap</button>
<script>
document.getElementById("clap").addEventListener("clap", (e) => {
fetch("/posts/42/claps", { method: "POST" });
});
</script>