Testimonials
Testimonials Slideshow
Heading over a photo, then client quotes in an auto-playing, draggable slideshow.
Layout and motion from the Nitip template — see it in context"use client";
import { Slideshow } from "@/components/ui/slideshow";
import { cn } from "@/lib/utils";
/*
* Testimonials Slideshow — a label and heading over a photo, then client quotes in an auto-playing slideshow on a
* plain panel.
* Source: marcframe/nitip → components/sections/TestimonialsSection.tsx (+ design-system TestimonialSlideshow,
* TestimonialCard).
*
* Layout kept from Framer: a photo box with a dark overlay holds the heading (up to 36rem) and, below it, the
* slideshow panel (padding 20 / 32 / 48). Slides are the panel's width on phones and 600px from 768px, so the next
* one peeks in. Each slide: the quote, then the client's portrait (80 × 96), name, company and service.
* Motion kept (slideshow): 5s autoplay, endless loop, drag or swipe, arrows and ← → keys; the slide spring is
* stiffness 200, damping 40; autoplay pauses on hover, focus, drag and while out of view.
* Dropped: the company logos and Nitip's dotted label. Text over the photo is white on a black overlay in every
* theme — photos don't follow the theme.
*/
export type Testimonial = {
quote: string;
name: string;
company: string;
/** What you did for them, shown under the name. */
service: string;
photo: { src: string; alt: string };
};
export type TestimonialsSlideshowProps = {
label?: string;
title?: string;
background?: { src: string; alt: string };
testimonials?: Testimonial[];
/** Milliseconds between slides; 0 turns autoplay off. */
interval?: number;
className?: string;
};
const face = (id: string) => `https://images.unsplash.com/${id}?auto=format&fit=crop&crop=faces&w=160&h=192&q=80`;
const defaults = {
label: "Testimonials",
title: "What clients say about working with us",
background: {
src: "https://images.unsplash.com/photo-1497366811353-6870744d04b2?auto=format&fit=crop&w=2000&q=80",
alt: "",
},
testimonials: [
{
quote: "Bookings climbed every month after launch. They treat our budget as if it were their own, and it shows.",
name: "Daniel Reyes",
company: "Harvest Kitchen",
service: "Social media management",
photo: { src: face("photo-1500648767791-00dcc994a43e"), alt: "Daniel Reyes" },
},
{
quote: "The content plan finally gave our small team a rhythm we can keep. Engagement doubled in one quarter.",
name: "Maya Collins",
company: "Verde Plant Co.",
service: "Content & copywriting",
photo: { src: face("photo-1534528741775-53994a69daeb"), alt: "Maya Collins" },
},
{
quote: "Clear numbers, no fluff. We cut our cost per lead by a third and know exactly where every dollar goes.",
name: "Tom Walsh",
company: "Pulse Fitness",
service: "Paid advertising",
photo: { src: face("photo-1560250097-0b93528c311a"), alt: "Tom Walsh" },
},
],
};
export function TestimonialsSlideshow({
label = defaults.label,
title = defaults.title,
background = defaults.background,
testimonials = defaults.testimonials,
interval = 5000,
className,
}: TestimonialsSlideshowProps) {
return (
<section className={cn("py-(--section-y)", className)}>
<div className="mx-auto max-w-(--container) px-(--gutter)">
<div className="relative overflow-hidden rounded-xl bg-muted">
<img src={background.src} alt={background.alt} className="absolute inset-0 size-full object-cover" />
<div className="relative flex flex-col gap-16 bg-black/50 p-5 md:gap-20 md:p-8 lg:gap-24 lg:p-10">
<div className="flex max-w-xl flex-col gap-5 text-white">
{label && <p className="text-sm font-medium text-white/80">{label}</p>}
<h2 className="font-heading text-h2 font-semibold tracking-tight text-balance">{title}</h2>
</div>
<Slideshow
items={testimonials}
getKey={(item) => item.name}
label={label || title}
interval={interval}
gap={64}
className="bg-background p-5 pb-16 text-foreground md:p-8 md:pb-16 lg:p-12 lg:pb-16"
slideClassName="w-full md:w-150"
renderItem={(item) => (
<figure className="flex h-full flex-col justify-between gap-8">
<blockquote className="text-h3 font-medium text-pretty">{item.quote}</blockquote>
<figcaption className="flex items-center gap-3">
<img
src={item.photo.src}
alt={item.photo.alt}
draggable={false}
className="h-24 w-20 shrink-0 rounded-md bg-muted object-cover"
/>
<span className="flex flex-col gap-1">
<span className="text-muted-foreground">
— {item.name}, {item.company}
</span>
<span className="font-semibold">{item.service}</span>
</span>
</figcaption>
</figure>
)}
/>
</div>
</div>
</div>
</section>
);
}Installation
$ npx shadcn@latest add https://framebrick.co/r/blocks/testimonials-slideshow.json1. Add the Framebrick UI components it uses (they bring Motion).
npx shadcn@latest add https://framebrick.co/r/slideshow.json2. Add the block tokens to
app/globals.cssonce (theming).npx shadcn@latest add https://framebrick.co/r/blocks/tokens.json3. Copy the block into
components/testimonials-slideshow.tsx."use client"; import { Slideshow } from "@/components/ui/slideshow"; import { cn } from "@/lib/utils"; /* * Testimonials Slideshow — a label and heading over a photo, then client quotes in an auto-playing slideshow on a * plain panel. * Source: marcframe/nitip → components/sections/TestimonialsSection.tsx (+ design-system TestimonialSlideshow, * TestimonialCard). * * Layout kept from Framer: a photo box with a dark overlay holds the heading (up to 36rem) and, below it, the * slideshow panel (padding 20 / 32 / 48). Slides are the panel's width on phones and 600px from 768px, so the next * one peeks in. Each slide: the quote, then the client's portrait (80 × 96), name, company and service. * Motion kept (slideshow): 5s autoplay, endless loop, drag or swipe, arrows and ← → keys; the slide spring is * stiffness 200, damping 40; autoplay pauses on hover, focus, drag and while out of view. * Dropped: the company logos and Nitip's dotted label. Text over the photo is white on a black overlay in every * theme — photos don't follow the theme. */ export type Testimonial = { quote: string; name: string; company: string; /** What you did for them, shown under the name. */ service: string; photo: { src: string; alt: string }; }; export type TestimonialsSlideshowProps = { label?: string; title?: string; background?: { src: string; alt: string }; testimonials?: Testimonial[]; /** Milliseconds between slides; 0 turns autoplay off. */ interval?: number; className?: string; }; const face = (id: string) => `https://images.unsplash.com/${id}?auto=format&fit=crop&crop=faces&w=160&h=192&q=80`; const defaults = { label: "Testimonials", title: "What clients say about working with us", background: { src: "https://images.unsplash.com/photo-1497366811353-6870744d04b2?auto=format&fit=crop&w=2000&q=80", alt: "", }, testimonials: [ { quote: "Bookings climbed every month after launch. They treat our budget as if it were their own, and it shows.", name: "Daniel Reyes", company: "Harvest Kitchen", service: "Social media management", photo: { src: face("photo-1500648767791-00dcc994a43e"), alt: "Daniel Reyes" }, }, { quote: "The content plan finally gave our small team a rhythm we can keep. Engagement doubled in one quarter.", name: "Maya Collins", company: "Verde Plant Co.", service: "Content & copywriting", photo: { src: face("photo-1534528741775-53994a69daeb"), alt: "Maya Collins" }, }, { quote: "Clear numbers, no fluff. We cut our cost per lead by a third and know exactly where every dollar goes.", name: "Tom Walsh", company: "Pulse Fitness", service: "Paid advertising", photo: { src: face("photo-1560250097-0b93528c311a"), alt: "Tom Walsh" }, }, ], }; export function TestimonialsSlideshow({ label = defaults.label, title = defaults.title, background = defaults.background, testimonials = defaults.testimonials, interval = 5000, className, }: TestimonialsSlideshowProps) { return ( <section className={cn("py-(--section-y)", className)}> <div className="mx-auto max-w-(--container) px-(--gutter)"> <div className="relative overflow-hidden rounded-xl bg-muted"> <img src={background.src} alt={background.alt} className="absolute inset-0 size-full object-cover" /> <div className="relative flex flex-col gap-16 bg-black/50 p-5 md:gap-20 md:p-8 lg:gap-24 lg:p-10"> <div className="flex max-w-xl flex-col gap-5 text-white"> {label && <p className="text-sm font-medium text-white/80">{label}</p>} <h2 className="font-heading text-h2 font-semibold tracking-tight text-balance">{title}</h2> </div> <Slideshow items={testimonials} getKey={(item) => item.name} label={label || title} interval={interval} gap={64} className="bg-background p-5 pb-16 text-foreground md:p-8 md:pb-16 lg:p-12 lg:pb-16" slideClassName="w-full md:w-150" renderItem={(item) => ( <figure className="flex h-full flex-col justify-between gap-8"> <blockquote className="text-h3 font-medium text-pretty">{item.quote}</blockquote> <figcaption className="flex items-center gap-3"> <img src={item.photo.src} alt={item.photo.alt} draggable={false} className="h-24 w-20 shrink-0 rounded-md bg-muted object-cover" /> <span className="flex flex-col gap-1"> <span className="text-muted-foreground"> — {item.name}, {item.company} </span> <span className="font-semibold">{item.service}</span> </span> </figcaption> </figure> )} /> </div> </div> </div> </section> ); }
Usage
import { TestimonialsSlideshow } from "@/components/testimonials-slideshow";
<TestimonialsSlideshow
title="What clients say"
background={{ src: "/office.jpg", alt: "" }}
testimonials={[
{ quote: "…", name: "Daniel Reyes", company: "Harvest Kitchen", service: "Social media", photo: { src: "/clients/daniel.jpg", alt: "Daniel Reyes" } },
]}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| label / title | string | — | Small line and heading (h2) over the photo; the label also names the carousel. |
| background | { src, alt } | — | Photo behind everything, under a dark overlay (decorative: alt ""). |
| testimonials | { quote, name, company, service, photo }[] | 3 quotes | One slide each. |
| interval | number | 5000 | Milliseconds between slides; 0 turns autoplay off. |
| className | string | — | Classes for the <section>. |
- Slides are the panel's width on phones and 600px from 768px, so the next one peeks in.
- 5s autoplay, endless loop, drag or swipe, arrows and ← → keys; autoplay pauses on hover, focus, drag and out of view (slideshow).
- Text over the photo is white on a black overlay in every theme. The company logos from Nitip are left out.
AI prompt
Paste into v0, Lovable, Bolt or Claude to rebuild this block in your own project.
Create a React + Tailwind CSS + Motion block called TestimonialsSlideshow (Next.js). - <section className="py-(--section-y)">, container mx-auto max-w-(--container) px-(--gutter). Inside, a rounded-xl box with a cover photo and a bg-black/50 overlay, padding p-5 / md:p-8 / lg:p-10, gap-16 / 20 / 24: white text — a small label (white/80) and an h2 (font-heading text-h2 semibold) up to max-w-xl — then the slideshow. - Slideshow: a bg-background panel (rounded-lg, p-5 / md:p-8 / lg:p-12, room at the bottom for two round arrow buttons at the bottom right). An endless track of slides (w-full, 600px from 768px, gap 64px) moved with a spring (stiffness 200, damping 40), draggable, 5s autoplay that pauses on hover, focus, drag and out of view; ← → keys; hidden slides aria-hidden. - Slide: a <figure> with a blockquote (text-h3 font-medium) and a figcaption: an 80×96 rounded portrait, "— Name, Company" muted and the service (semibold). - Only shadcn variables and block tokens (white and black only over the photo).











