← Feedback

Thumbs Up Feedback

A little “Was this helpful?” card with thumbs up and down. Pick one and the thumb pops, fills with colour and throws off a ring of sparks, then the card flips over on its middle, folding the question away and unfolding a thank-you in its place. Try it.

Was this helpful?

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-thumbs-up-feedback to an element holding your question. The thumbs and the thank-you are built around it.
  3. Listen for its feedback event to record the answer: e.detail.value is "up" or "down".

Options

Add any of these attributes to the same element.

data-thanksThe thank-you. Default “Thanks for your feedback!”.
data-thanks-downA different thank-you after a thumbs down.
data-colorThumbs-up colour. Default #6a0dad.
data-down-colorThumbs-down colour. Default #475569.
data-resetFold back to the question after this many ms, so it can be answered again. Off by default.
data-autoplayGive a thumbs up by itself while in view and fold back again, until someone clicks. For demos.

The thumbs are real buttons labelled “Yes” and “No” with aria-pressed, and the thank-you is a status region so screen readers hear it. With reduced motion the thank-you simply replaces the question.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Thumbs Up Feedback: a "Was this helpful?" card with thumbs up and down.
    // Pick one and the thumb pops, fills with colour and throws off a few
    // sparks; then the card flips over on its middle, folding the question
    // away and unfolding a thank-you in its place.
    // Usage: <div data-thumbs-up-feedback>Was this helpful?</div>   (the text is the question)
    // Optional: data-thanks (default "Thanks for your feedback!"),
    // data-thanks-down (after a thumbs down, default the same as data-thanks),
    // data-color (thumbs-up colour, default #6a0dad), data-down-color
    // (default #475569), data-reset (ms after which it folds back to the
    // question; off by default), data-autoplay (gives a thumbs up by itself
    // while in view and folds back again, until someone clicks; for demos).
    // Listen for the "feedback" event: e.detail.value is "up" or "down".
    var DEFAULTS = { thanks: 'Thanks for your feedback!', color: '#6a0dad', downColor: '#475569' };
    var THUMB = '<svg viewBox="0 0 24 24" aria-hidden="true"><path class="tfb-hand" d="M7 10.5l4-8.5a3 3 0 0 1 3 3v4h5.3a2 2 0 0 1 2 2.3l-1.4 9a2 2 0 0 1-2 1.7H7z"/><path d="M7 22H4a2 2 0 0 1-2-2v-7.5a2 2 0 0 1 2-2h3"/></svg>';
    var TICK = '<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M7.5 12.5l3 3l6-6.5"/></svg>';

    var css =
      '.tfb{display:inline-grid;perspective:40em;line-height:1.3;vertical-align:middle}' +
      '.tfb-face{grid-area:1/1;display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:.5em .8em;padding:.65em .8em .65em 1.1em;' +
      'border-radius:1em;background:var(--tfb-bg,#fff);box-shadow:0 .3em 1.2em rgba(20,15,40,.1);backface-visibility:hidden;transform-origin:50% 50%}' +
      '.tfb-q{font-weight:700}' +
      '.tfb-btns{display:inline-flex;gap:.4em}' +
      '.tfb-btn{position:relative;display:inline-grid;place-items:center;width:2.3em;height:2.3em;padding:0;border:1px solid rgba(120,110,140,.3);border-radius:50%;' +
      'background:none;color:inherit;font:inherit;cursor:pointer;-webkit-tap-highlight-color:transparent;transition:background .2s,border-color .2s}' +
      'button.tfb-btn:hover{background:rgba(120,110,140,.1)}' +
      'button.tfb-btn:focus-visible{outline:2px solid var(--tfb-c);outline-offset:2px}' +
      '.tfb-btn svg{width:1.15em;height:1.15em;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;overflow:visible}' +
      '.tfb-hand{fill:transparent}' +
      '.tfb-spark{position:absolute;left:50%;top:50%;width:.3em;height:.3em;margin:-.15em;border-radius:50%;background:var(--tfb-c);pointer-events:none}' +
      '.tfb-thanks{font-weight:700;padding-left:.9em;color:var(--tfb-c);visibility:hidden}' +
      '.tfb-thanks svg{width:1.3em;height:1.3em;flex:none;fill:none;stroke:currentColor;stroke-width:2.2;stroke-linecap:round;stroke-linejoin:round}' +
      '.tfb-thanks span{color:var(--tfb-text,inherit)}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
    function opt(el, k) { var v = el.dataset[k]; return v === undefined ? DEFAULTS[k] : v; }

    function setup(el) {
      var question = el.textContent.trim() || 'Was this helpful?';
      // Inside a card thumb (a link), use spans rather than real buttons.
      var tag = el.closest('[aria-hidden="true"]') ? 'span' : 'button';
      var btn = function (cls, label) {
        return '<' + tag + (tag === 'button' ? ' type="button" aria-pressed="false"' : '') + ' class="tfb-btn ' + cls + '" aria-label="' + label + '">' + THUMB + '</' + tag + '>';
      };
      el.classList.add('tfb');
      el.style.setProperty('--tfb-c', opt(el, 'color'));
      el.innerHTML =
        '<div class="tfb-face tfb-ask"><span class="tfb-q"></span><span class="tfb-btns">' + btn('tfb-up', 'Yes') + btn('tfb-down', 'No') + '</span></div>' +
        '<div class="tfb-face tfb-thanks" role="status">' + TICK + '<span></span></div>';
      el.querySelector('.tfb-q').textContent = question;
      var s = el._tfb = {
        ask: el.querySelector('.tfb-ask'), thanks: el.querySelector('.tfb-thanks'),
        up: el.querySelector('.tfb-up'), down: el.querySelector('.tfb-down'),
        done: false, timer: null, auto: false, visible: false
      };
      anime.utils.set(s.down.querySelector('svg'), { rotate: 180 });
      [s.up, s.down].forEach(function (b) {
        b.addEventListener('click', function () {
          s.auto = false;
          pick(el, b === s.up ? 'up' : 'down');
        });
      });
    }

    function sparks(btn, color) {
      for (var i = 0; i < 8; i++) {
        var sp = document.createElement('span');
        sp.className = 'tfb-spark';
        sp.style.background = color;
        btn.appendChild(sp);
        var a = (i / 8) * Math.PI * 2 - Math.PI / 2, r = 1.7 + (i % 2) * 0.5;
        anime.animate(sp, {
          x: [0, Math.cos(a) * r + 'em'], y: [0, Math.sin(a) * r + 'em'], scale: [1, 0.2], opacity: [1, 0],
          duration: 650, ease: 'out(3)', onComplete: sp.remove.bind(sp)
        });
      }
    }

    function pick(el, value) {
      var s = el._tfb, d = el.dataset;
      if (s.done) return;
      s.done = true;
      clearTimeout(s.timer);
      var up = value === 'up';
      var btn = up ? s.up : s.down, other = up ? s.down : s.up;
      var color = up ? opt(el, 'color') : opt(el, 'downColor');
      el.style.setProperty('--tfb-c', color);
      if (btn.tagName === 'BUTTON') btn.setAttribute('aria-pressed', 'true');
      s.thanks.querySelector('span').textContent = (!up && d.thanksDown) || opt(el, 'thanks');
      el.dispatchEvent(new CustomEvent('feedback', { bubbles: true, detail: { value: value } }));
      var hand = btn.querySelector('.tfb-hand');
      if (reduced()) {
        s.ask.style.visibility = 'hidden';
        s.thanks.style.visibility = 'visible';
      } else {
        // Pop the thumb, then flip the card over.
        btn.style.color = color;
        btn.style.borderColor = color;
        anime.animate(hand, { fill: color, duration: 200, ease: 'out(2)' });
        anime.animate(btn.querySelector('svg'), {
          scale: [{ to: 1.5, duration: 180, ease: 'out(2)' }, { to: 1, duration: 600, ease: 'outElastic(1, .45)' }],
          rotate: up ? [{ to: -18, duration: 180 }, { to: 0, duration: 600, ease: 'outElastic(1, .45)' }] : [{ to: 162, duration: 180 }, { to: 180, duration: 600, ease: 'outElastic(1, .45)' }]
        });
        anime.animate(other, { opacity: 0.3, scale: 0.85, duration: 300, ease: 'out(2)' });
        sparks(btn, color);
        anime.animate(s.ask, { rotateX: [0, 90], duration: 300, delay: 700, ease: 'in(2)', onComplete: function () { s.ask.style.visibility = 'hidden'; } });
        s.thanks.style.visibility = 'visible';
        anime.utils.set(s.thanks, { rotateX: -90 });
        anime.animate(s.thanks, { rotateX: [-90, 0], duration: 700, delay: 1000, ease: 'outBack(1.4)' });
        anime.animate(s.thanks.querySelector('svg'), { scale: [0, 1], rotate: [-45, 0], duration: 600, delay: 1200, ease: 'outBack(2.2)' });
      }
      var reset = +d.reset || (s.auto ? 2800 : 0);
      if (reset) s.timer = setTimeout(function () { unpick(el); }, reset + 1000);
    }

    // Fold back to the question.
    function unpick(el) {
      var s = el._tfb;
      [s.up, s.down].forEach(function (b) {
        b.style.color = b.style.borderColor = '';
        if (b.tagName === 'BUTTON') b.setAttribute('aria-pressed', 'false');
        anime.utils.set(b, { opacity: 1, scale: 1 });
        anime.utils.set(b.querySelector('.tfb-hand'), { fill: 'rgba(0,0,0,0)' });
      });
      el.style.setProperty('--tfb-c', opt(el, 'color'));
      var back = function () { s.done = false; if (s.auto) s.timer = setTimeout(function () { autoStep(el); }, 1600); };
      if (reduced()) {
        s.thanks.style.visibility = 'hidden';
        s.ask.style.visibility = 'visible';
        back();
        return;
      }
      anime.animate(s.thanks, { rotateX: [0, 90], duration: 300, ease: 'in(2)', onComplete: function () { s.thanks.style.visibility = 'hidden'; } });
      s.ask.style.visibility = 'visible';
      anime.animate(s.ask, { rotateX: [-90, 0], duration: 650, delay: 300, ease: 'outBack(1.4)', onComplete: back });
    }

    function autoStep(el) {
      var s = el._tfb;
      if (!s.auto || s.done) return;
      if (!s.visible) { s.waiting = true; return; }
      // A pretend press on the thumbs up.
      anime.animate(s.up, { scale: [{ to: 0.85, duration: 120 }, { to: 1, duration: 200 }], ease: 'out(2)', onComplete: function () { if (s.auto) pick(el, 'up'); } });
    }

    function init() {
      var io = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          var s = e.target._tfb;
          s.visible = e.isIntersecting;
          if (s.visible && s.auto && (s.waiting || !s.started)) {
            s.started = true;
            s.waiting = false;
            clearTimeout(s.timer);
            s.timer = setTimeout(function () { autoStep(e.target); }, 1200);
          }
        });
      }, { threshold: 0.4 });
      document.querySelectorAll('[data-thumbs-up-feedback]').forEach(function (el) {
        if (el._tfb) return;
        setup(el);
        if (el.dataset.autoplay !== undefined && el.dataset.autoplay !== 'false' && !reduced()) {
          el._tfb.auto = true;
          io.observe(el);
        }
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-thumbs-up-feedback>Was this helpful?</div>

<script>
  document.addEventListener("feedback", (e) => {
    fetch("/api/feedback", { method: "POST", body: e.detail.value });
  });
</script>