← Buttons

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

  1. Paste the snippet just before </head>.
  2. Add data-clap-counter to a button, with the current total in data-count. Its text becomes its name for screen readers.
  3. Save each clap when it fires its clap event.

Options

Add any of these attributes to the same element.

data-countThe starting total. Default 0.
data-maxMost claps one visitor can give; after that it just shakes. Default 50.
data-iconThe icon. Default 👏.
data-colorColour of the burst and bubble. Default #6a0dad.
data-autoplayIt 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.

1 · Snippet
<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>
2 · Your HTML
<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>