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
- Paste the snippet just before
</head>. - Add
data-thumbs-up-feedbackto an element holding your question. The thumbs and the thank-you are built around it. - Listen for its
feedbackevent to record the answer:e.detail.valueis"up"or"down".
Options
Add any of these attributes to the same element.
data-thanks | The thank-you. Default “Thanks for your feedback!”. |
data-thanks-down | A different thank-you after a thumbs down. |
data-color | Thumbs-up colour. Default #6a0dad. |
data-down-color | Thumbs-down colour. Default #475569. |
data-reset | Fold back to the question after this many ms, so it can be answered again. Off by default. |
data-autoplay | Give 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.
<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><div data-thumbs-up-feedback>Was this helpful?</div>
<script>
document.addEventListener("feedback", (e) => {
fetch("/api/feedback", { method: "POST", body: e.detail.value });
});
</script>