Testimonials
Testimonials — tabbed by industry
Tabs for each kind of customer that switch between a quote from that industry and two of its results.
4 industry tabs · quote + person · results panel in brand tint · switching panels · stacks on small screens
Built for your kind of business
Hear it from someone like you
“Patients confirm or move their own appointments from the reminder. The front desk spends mornings on people in the room, not on the phone.”
- missed appointments
- −42%
- of phone calls saved daily
- 3 hrs
“Every chair has its own calendar and clients pick the stylist they want. Saturdays are booked out by Wednesday.”
- chairs booked on weekends
- 94%
- to set up the team
- 11 min
“Class packs, waitlists and reminders all in one place. When someone cancels, the next person on the waitlist gets the spot automatically.”
- empty spots after cancellations
- 0
- class-pack renewals
- 2×
“Customers book a visit window from my website and the address lands in my calendar. I quote on site and the deposit is already paid.”
- jobs with a deposit up front
- 9 / 10
- of admin saved weekly
- 5 hrs
"use client";
import { useRef, useState } from "react";
import { ArrowRight } from "lucide-react";
import { cn } from "@/lib/utils";
const industries = [
{ id: "clinics", label: "Clinics", quote: "Patients confirm or move their own appointments from the reminder. The front desk spends mornings on people in the room, not on the phone.", name: "Dr. Amara Osei", role: "Practice lead, Fernhill Dental", results: [{ value: "−42%", label: "missed appointments" }, { value: "3 hrs", label: "of phone calls saved daily" }] },
{ id: "salons", label: "Salons", quote: "Every chair has its own calendar and clients pick the stylist they want. Saturdays are booked out by Wednesday.", name: "Leah Moreno", role: "Owner, Loft Hair Studio", results: [{ value: "94%", label: "chairs booked on weekends" }, { value: "11 min", label: "to set up the team" }] },
{ id: "studios", label: "Studios", quote: "Class packs, waitlists and reminders all in one place. When someone cancels, the next person on the waitlist gets the spot automatically.", name: "Ines Duarte", role: "Founder, Core & Coast Pilates", results: [{ value: "0", label: "empty spots after cancellations" }, { value: "2×", label: "class-pack renewals" }] },
{ id: "trades", label: "Trades", quote: "Customers book a visit window from my website and the address lands in my calendar. I quote on site and the deposit is already paid.", name: "Callum Reid", role: "Reid Plumbing & Heating", results: [{ value: "9 / 10", label: "jobs with a deposit up front" }, { value: "5 hrs", label: "of admin saved weekly" }] },
];
const initials = (name: string) => name.replace("Dr. ", "").split(" ").map((w) => w[0]).join("").slice(0, 2);
export function TestimonialsByIndustry() {
const [active, setActive] = useState(0);
const refs = useRef<(HTMLButtonElement | null)[]>([]);
const onKeyDown = (e: React.KeyboardEvent) => {
const last = industries.length - 1;
const keys: Record<string, number> = {
ArrowRight: active === last ? 0 : active + 1,
ArrowLeft: active === 0 ? last : active - 1,
Home: 0,
End: last,
};
const next = keys[e.key];
if (next === undefined) return;
e.preventDefault();
setActive(next);
refs.current[next]?.focus();
};
return (
<section aria-labelledby="industry-title" className="bg-bg px-5 py-12 font-sans sm:px-10 sm:py-20">
<div className="mx-auto grid max-w-6xl gap-7">
<div className="mx-auto grid max-w-2xl justify-items-center gap-3.5 text-center">
<p className="text-[13px] font-medium text-primary">Built for your kind of business</p>
<h2 id="industry-title" className="text-balance text-3xl font-semibold tracking-tight text-fg sm:text-4xl">Hear it from someone like you</h2>
</div>
<div role="tablist" aria-label="Industry" className="flex flex-wrap justify-center gap-1.5">
{industries.map((d, i) => {
const selected = active === i;
return (
<button
key={d.id}
ref={(el) => { refs.current[i] = el; }}
id={"industry-tab-" + d.id}
type="button"
role="tab"
aria-selected={selected}
aria-controls={"industry-panel-" + d.id}
tabIndex={selected ? 0 : -1}
onClick={() => setActive(i)}
onKeyDown={onKeyDown}
className={cn(
"h-8 rounded-full border px-3.5 text-[13px] font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
selected ? "border-fg bg-fg text-bg" : "border-border bg-bg text-fg-muted hover:border-border-strong hover:text-fg",
)}
>
{d.label}
</button>
);
})}
</div>
{industries.map((d, i) => (
<div key={d.id} id={"industry-panel-" + d.id} role="tabpanel" aria-labelledby={"industry-tab-" + d.id} hidden={active !== i} tabIndex={0}>
<div className="grid overflow-hidden rounded-xl border border-border bg-bg md:grid-cols-[1.6fr_1fr]">
<figure className="grid content-between gap-7 p-7 md:p-10">
<blockquote className="text-pretty text-xl font-medium leading-snug tracking-tight text-fg sm:text-[25px]">
<p>“{d.quote}”</p>
</blockquote>
<figcaption className="flex items-center gap-3">
<span className="grid size-9 place-items-center rounded-full bg-primary-soft text-[13px] font-medium text-primary-soft-fg" aria-hidden>{initials(d.name)}</span>
<div>
<p className="text-sm font-semibold text-fg">{d.name}</p>
<p className="text-[13px] text-fg-muted">{d.role}</p>
</div>
</figcaption>
</figure>
<div className="grid content-between gap-6 bg-primary-soft p-7 text-primary-soft-fg md:p-10">
<dl className="grid gap-[18px]">
{d.results.map((r) => (
<div key={r.label}>
<dt className="text-[13px] opacity-85">{r.label}</dt>
<dd className="mt-0.5 text-4xl font-semibold tabular-nums tracking-tight">{r.value}</dd>
</div>
))}
</dl>
<a href={"/customers/" + d.id} className="inline-flex items-center gap-1.5 text-sm font-medium hover:underline">
Read the full story <ArrowRight className="size-4" aria-hidden />
</a>
</div>
</div>
</div>
))}
</div>
</section>
);
}
The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.