Build a testimonial switcher with Tailwind CSS controlled by a row of avatars. Centre a max-w-3xl column. At the top, a horizontal row of five circular avatars with initials on coloured backgrounds; the active one is full size with a neutral-900 ring and the others are slightly smaller and dimmed. Below, the quote panel: a company wordmark in muted uppercase, a large quote in relaxed leading, an attribution block with name and role, and a small row of three metric chips with a label and value. Under the panel, a pair of previous and next chevron buttons on the left and a set of five thin progress dashes on the right where the active one is filled. Then add JavaScript that renders the panel from a data array, wires the avatars and the chevrons, updates the ring and dimming states, advances automatically every seven seconds, and pauses the timer when the pointer is over the panel.
Paste it into Claude, Cursor, v0 or any AI coding tool. Prefer the
finished markup? .
Build a testimonial switcher with Tailwind CSS controlled by a row of avatars. Centre a max-w-3xl column. At the top, a horizontal row of five circular avatars with initials on coloured backgrounds; the active one is full size with a neutral-900 ring and the others are slightly smaller and dimmed. Below, the quote panel: a company wordmark in muted uppercase, a large quote in relaxed leading, an attribution block with name and role, and a small row of three metric chips with a label and value. Under the panel, a pair of previous and next chevron buttons on the left and a set of five thin progress dashes on the right where the active one is filled. Then add JavaScript that renders the panel from a data array, wires the avatars and the chevrons, updates the ring and dimming states, advances automatically every seven seconds, and pauses the timer when the pointer is over the panel.
var QUOTES = [
{
company: "Fernbrook Health",
text: "Three days of finance work turned into a scheduled job that nobody has touched since March.",
name: "Aiko Tan",
role: "Financial Controller",
chips: ["Close time −94%", "3 days saved monthly", "2× audit coverage"],
},
{
company: "Kestrel Logistics",
text: "Depot managers were building their own reports by week two, which is not how these projects usually go.",
name: "Priya Mahal",
role: "Head of Operations",
chips: ["Live in 14 days", "On-time drops +41%", "62 depots"],
},
{
company: "Northwind",
text: "Support answered on a Sunday with an actual query plan, then fixed the documentation that caused the confusion.",
name: "Mira Osei",
role: "Staff Engineer",
chips: ["41 min median reply", "0 escalations", "99.98% uptime"],
},
{
company: "Oakline Group",
text: "Our auditor asked for evidence and I sent one link. That was the entire meeting.",
name: "Lena Rask",
role: "Compliance Lead",
chips: ["SOC 2 in 6 weeks", "1 link, 0 spreadsheets", "210 seats"],
},
{
company: "Corvid Studio",
text: "We migrated 2.1 billion rows over a weekend with zero incidents and a runbook that matched reality.",
name: "Tom Silva",
role: "Principal Data Engineer",
chips: ["2.1B rows", "0 incidents", "48h cutover"],
},
];
var avatars = Array.prototype.slice.call(document.querySelectorAll(".qa"));
var dots = Array.prototype.slice.call(document.getElementById("quoteDots").children);
var panel = document.getElementById("quotePanel");
var company = document.getElementById("quoteCompany");
var text = document.getElementById("quoteText");
var name = document.getElementById("quoteName");
var role = document.getElementById("quoteRole");
var chips = document.getElementById("quoteChips");
var index = 0;
var timer;
function show(i) {
index = (i + QUOTES.length) % QUOTES.length;
var q = QUOTES[index];
company.textContent = q.company;
text.textContent = q.text;
name.textContent = q.name;
role.textContent = q.role;
chips.innerHTML = q.chips
.map(function (c) {
return '<span class="rounded-full bg-neutral-100 px-3 py-1 text-xs text-neutral-600">' + c + "</span>";
})
.join("");
avatars.forEach(function (a, n) {
var on = n === index;
a.classList.toggle("ring-2", on);
a.classList.toggle("ring-neutral-900", on);
a.classList.toggle("ring-offset-2", on);
a.classList.toggle("opacity-40", !on);
a.classList.toggle("scale-90", !on);
});
dots.forEach(function (d, n) {
d.classList.toggle("bg-neutral-900", n === index);
d.classList.toggle("bg-neutral-200", n !== index);
});
}
function start() {
stop();
timer = setInterval(function () {
show(index + 1);
}, 7000);
}
function stop() {
clearInterval(timer);
}
avatars.forEach(function (a) {
a.addEventListener("click", function () {
show(Number(a.dataset.i));
start();
});
});
document.getElementById("quotePrev").addEventListener("click", function () {
show(index - 1);
start();
});
document.getElementById("quoteNext").addEventListener("click", function () {
show(index + 1);
start();
});
// Reading should not be interrupted by the carousel moving on.
panel.addEventListener("mouseenter", stop);
panel.addEventListener("mouseleave", start);
show(0);
start();