Testimonials
Testimonials — masonry wall
A wall of short and long quotes in staggered columns that fades out, with a button to show more.
1 → 2 → 3 columns · tinted and large cards · fade + show more
What people say after their first month
Sample notes from tradespeople, studios and shops who send their invoices with Tallybook.
We used to spend the last Friday of every month chasing payments. Now the reminders go out on their own and we spend that Friday on actual work.
Setup took one coffee.
Clients pay from the email now. I didn’t realise how many were waiting for a link.
The recurring invoices handle our forty retainer clients without anyone touching them. That was a full day a month for me before.
My accountant asked what changed, because the books finally matched the bank.
Late fees used to feel awkward to add. Now they’re just part of the terms, and almost nobody pays late anymore.
I send quotes from my phone in the van and they turn into invoices when the job’s done.
Simple enough for me, detailed enough for my accountant.
"use client";
import { useState } from "react";
import { ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils";
type Quote = { quote: string; name: string; role: string; size?: "lg"; tint?: boolean };
const quotes: Quote[] = [
{ quote: "We used to spend the last Friday of every month chasing payments. Now the reminders go out on their own and we spend that Friday on actual work.", name: "Priya Natarajan", role: "Owner, Lantern Joinery", size: "lg" },
{ quote: "Setup took one coffee.", name: "Tom Alvarez", role: "Plumber" },
{ quote: "Clients pay from the email now. I didn’t realise how many were waiting for a link.", name: "Mei Lin", role: "Bookkeeper, Lin & Co", tint: true },
{ quote: "The recurring invoices handle our forty retainer clients without anyone touching them.", name: "Kwame Mensah", role: "Ops lead, Northfold Studio" },
{ quote: "My accountant asked what changed, because the books finally matched the bank.", name: "Sara Lindqvist", role: "Ceramicist" },
{ quote: "Late fees used to feel awkward to add. Now they’re just part of the terms.", name: "Diego Romero", role: "Founder, Romero Events", tint: true },
{ quote: "I send quotes from my phone in the van and they turn into invoices when the job’s done.", name: "Hannah Brooks", role: "Electrician" },
{ quote: "Simple enough for me, detailed enough for my accountant.", name: "Arjun Desai", role: "Photographer", size: "lg" },
];
const initials = (name: string) => name.split(" ").map((w) => w[0]).join("").slice(0, 2);
export function TestimonialsMasonry() {
const [expanded, setExpanded] = useState(false);
return (
<section aria-labelledby="wall-title" className="bg-bg px-5 py-16 font-sans sm:px-10 sm:py-20">
<div className="mx-auto max-w-6xl">
<div className="mx-auto max-w-2xl text-center">
<h2 id="wall-title" className="text-balance text-3xl font-semibold tracking-tight text-fg sm:text-4xl">What people say after their first month</h2>
<p className="mt-4 text-base text-fg-muted sm:text-lg">Notes from tradespeople, studios and shops who send their invoices with Tallybook.</p>
</div>
<div className={cn("relative mt-10", !expanded && "max-h-[600px] overflow-hidden after:pointer-events-none after:absolute after:inset-x-0 after:bottom-0 after:h-36 after:bg-gradient-to-b after:from-transparent after:to-bg")}>
<div className="gap-4 sm:columns-2 lg:columns-3">
{quotes.map((q) => (
<figure
key={q.name}
className={cn("mb-4 grid break-inside-avoid gap-4 rounded-lg border border-border bg-bg p-5", q.tint && "border-transparent bg-primary-soft")}
>
<blockquote className={cn("text-[14.5px] leading-relaxed text-fg", q.size === "lg" && "text-[17px] font-medium tracking-tight")}>
<p>{q.quote}</p>
</blockquote>
<figcaption className="flex items-center gap-2.5">
<span className="grid size-7 place-items-center rounded-full bg-surface-2 text-[11px] font-medium text-fg-muted" aria-hidden>{initials(q.name)}</span>
<div>
<p className="text-[13.5px] font-semibold text-fg">{q.name}</p>
<p className="text-[12.5px] text-fg-muted">{q.role}</p>
</div>
</figcaption>
</figure>
))}
</div>
</div>
{!expanded && (
<div className="mt-2 flex justify-center">
<button
type="button"
onClick={() => setExpanded(true)}
className="inline-flex h-9 items-center gap-1.5 rounded border border-border-strong bg-bg px-3.5 text-sm font-medium text-fg transition hover:bg-surface"
>
Show more stories <ChevronDown className="size-4" aria-hidden />
</button>
</div>
)}
</div>
</section>
);
}
The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.