Testimonial Rotator
One quote at a time: the current one slides away and fades, the next slides in, its avatar pops into place with a little wobble and the name follows. Dots underneath jump straight to a quote.
“It took ten minutes to add and our landing page finally feels alive.”
“Copy, paste, done. I didn’t have to learn a single new tool.”
“The animations are subtle in the right way. Our clients noticed straight away.”
“Finally, motion that works on phones as well as it does on my big screen.”
How to use it
- Paste the snippet just before
</head>. - Add
data-testimonial-rotatorand anaria-labelto a container whose children are the testimonials, each a<figure>with a<blockquote>and a<figcaption>. The avatar is its<img>, or any element markeddata-avatar.
Options
Add any of these attributes to the same element.
data-interval | How long each quote stays. Default 5000 ms. |
data-controls="false" | No dots. |
The container keeps the height of the tallest quote, so nothing below it jumps. It pauses under the mouse, while it has focus and when it’s off screen; hidden quotes are hidden from screen readers too. From code, animacious.testimonial(element, 2) shows a quote.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Testimonial Rotator: shows one quote at a time. On each change the
// quote slides out and fades, the next one slides in, and its avatar pops
// into place with the name following. Dots underneath jump to a quote.
// Usage:
// <div data-testimonial-rotator aria-label="What people say">
// <figure>
// <blockquote>“Lovely to work with.”</blockquote>
// <figcaption><img src="ana.jpg" alt=""> <b>Ana Ruiz</b> Designer</figcaption>
// </figure>
// <figure>…</figure>
// </div>
// Each child is a testimonial. The avatar is its <img> or an element
// marked data-avatar.
// Optional: data-interval (ms each quote stays, default 5000),
// data-controls="false" (no dots).
// Script use: animacious.testimonial(element, index) shows a quote.
var DEFAULTS = { interval: 5000 };
var css =
'[data-testimonial-rotator]{display:grid}' +
'[data-testimonial-rotator]>:not(.tmr-dots){grid-area:1/1;margin:0}' +
'[data-testimonial-rotator]>.tmr-off{visibility:hidden}' +
'.tmr-dots{grid-area:2/1;display:flex;justify-content:center;gap:8px;margin-top:18px}' +
'.tmr-dots>*{width:8px;height:8px;padding:0;border:0;border-radius:4px;color:inherit;background:currentColor;opacity:.25;cursor:pointer;transition:width .35s,opacity .35s}' +
'.tmr-dots>[aria-current="true"],.tmr-dots>.tmr-on{width:24px;opacity:.8}' +
'.tmr-dots>button:focus-visible{outline:3px solid #a855f7;outline-offset:3px}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function parts(item) {
var avatar = item.querySelector('[data-avatar]') || item.querySelector('img');
var quote = item.querySelector('blockquote') || item.firstElementChild;
var cap = item.querySelector('figcaption, cite');
var rest = cap ? Array.from(cap.children).filter(function (c) { return c !== avatar && !c.contains(avatar); }) : [];
if (cap && !rest.length && !cap.contains(avatar)) rest = [cap];
return { avatar: avatar, quote: quote, rest: rest };
}
function setup(el) {
var d = el.dataset;
var items = Array.from(el.children);
var n = items.length;
if (!n) return;
var interval = +d.interval || DEFAULTS.interval;
var inThumb = !!el.closest('[aria-hidden="true"]');
var current = 0, timer = null, paused = false, visible = false;
if (!inThumb) {
el.setAttribute('role', 'region');
el.setAttribute('aria-roledescription', 'carousel');
}
items.forEach(function (it, i) {
it.classList.toggle('tmr-off', i !== 0);
if (!inThumb) it.setAttribute('aria-hidden', i === 0 ? 'false' : 'true');
});
var dots = [];
if (d.controls !== 'false' && n > 1) {
var bar = document.createElement('div');
bar.className = 'tmr-dots';
items.forEach(function (_, i) {
var dot = document.createElement(inThumb ? 'span' : 'button');
if (!inThumb) {
dot.type = 'button';
dot.setAttribute('aria-label', 'Show testimonial ' + (i + 1));
dot.addEventListener('click', function () { show(i); restart(); });
}
bar.appendChild(dot);
dots.push(dot);
});
el.appendChild(bar);
}
function mark() {
dots.forEach(function (dot, i) {
if (inThumb) dot.classList.toggle('tmr-on', i === current);
else dot.setAttribute('aria-current', i === current ? 'true' : 'false');
});
}
function show(i) {
i = ((i % n) + n) % n;
if (i === current) return;
var out = items[current], inn = items[i], po = parts(out), pi = parts(inn);
var dir = i > current ? 1 : -1;
current = i;
mark();
if (!inThumb) {
out.setAttribute('aria-hidden', 'true');
inn.setAttribute('aria-hidden', 'false');
}
inn.classList.remove('tmr-off');
if (reduced()) { out.classList.add('tmr-off'); return; }
anime.utils.remove([out, inn, po.quote, po.avatar, pi.quote, pi.avatar].concat(po.rest, pi.rest).filter(Boolean));
// Out: everything slides away the way we're going and fades.
anime.animate(out, {
x: -40 * dir, opacity: 0, duration: 380, ease: 'in(2)',
onComplete: function () { out.classList.add('tmr-off'); anime.utils.set(out, { x: 0, opacity: 1 }); }
});
// In: the quote slides in, the avatar pops, then the name follows.
anime.utils.set(inn, { x: 0, opacity: 1 });
if (pi.quote) anime.animate(pi.quote, { x: [50 * dir, 0], opacity: [0, 1], duration: 650, delay: 220, ease: 'out(3)' });
if (pi.avatar) anime.animate(pi.avatar, { scale: [0.4, 1], rotate: [-25 * dir, 0], opacity: [0, 1], duration: 900, delay: 380, ease: 'outElastic(1, .65)' });
if (pi.rest.length) anime.animate(pi.rest, { y: [10, 0], opacity: [0, 1], duration: 500, delay: anime.stagger(60, { start: 480 }), ease: 'out(3)' });
}
el._tmr = function (i) { show(i); restart(); };
function restart() {
clearInterval(timer);
timer = null;
if (!visible || reduced() || n < 2) return;
timer = setInterval(function () {
if (!paused && !el.closest('[hidden]')) show(current + 1);
}, interval);
}
if (!inThumb) {
el.addEventListener('pointerenter', function (e) { if (e.pointerType === 'mouse') paused = true; });
el.addEventListener('pointerleave', function () { paused = false; });
el.addEventListener('focusin', function () { paused = true; });
el.addEventListener('focusout', function () { paused = false; });
}
mark();
new IntersectionObserver(function (entries) {
visible = entries[0].isIntersecting;
restart();
}).observe(el);
}
function init() {
document.querySelectorAll('[data-testimonial-rotator]').forEach(function (el) {
if (el._tmr) return;
el._tmr = true;
setup(el);
});
}
window.animacious = window.animacious || {};
window.animacious.testimonial = function (el, i) { if (el && typeof el._tmr === 'function') el._tmr(i); };
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-testimonial-rotator aria-label="What people say">
<figure>
<blockquote>“Lovely to work with.”</blockquote>
<figcaption><img src="ana.jpg" alt=""> <b>Ana Ruiz</b> <span>Designer</span></figcaption>
</figure>
<figure>…</figure>
<figure>…</figure>
</div>