Testimonials
Customer testimonials with ratings.
Default Usage
import { Testimonials } from '@/components/landing';
<Testimonials />
Location: src/components/landing/Testimonials.tsx
Features
- Star ratings (1-5)
- Avatar support
- Author name, role, company
- Grid layout (1-2-3 columns)
Customization
const testimonials: Testimonial[] = [
{
quoteKey: 't1.quote',
author: {
nameKey: 't1.name',
roleKey: 't1.role',
companyKey: 't1.company',
avatar: '/images/testimonials/avatar1.jpg',
},
rating: 5,
},
];
Variants
Variant 1: Grid (Default)
3-column grid with cards.
// src/components/landing/Testimonials.tsx - Default implementation
Best for: Multiple testimonials, balanced display
Variant 2: Marquee/Carousel
Auto-scrolling testimonials.
// src/components/landing/TestimonialsMarquee.tsx
'use client';
import { useTranslations } from 'next-intl';
import Image from 'next/image';
import { StarIcon, QuoteIcon } from '@/components/icons';
const testimonials = [
{
quoteKey: 't1.quote',
author: { nameKey: 't1.name', roleKey: 't1.role', avatar: '/images/avatar1.jpg' },
rating: 5,
},
{
quoteKey: 't2.quote',
author: { nameKey: 't2.name', roleKey: 't2.role', avatar: '/images/avatar2.jpg' },
rating: 5,
},
{
quoteKey: 't3.quote',
author: { nameKey: 't3.name', roleKey: 't3.role', avatar: '/images/avatar3.jpg' },
rating: 5,
},
// Duplicate for seamless loop
{
quoteKey: 't1.quote',
author: { nameKey: 't1.name', roleKey: 't1.role', avatar: '/images/avatar1.jpg' },
rating: 5,
},
{
quoteKey: 't2.quote',
author: { nameKey: 't2.name', roleKey: 't2.role', avatar: '/images/avatar2.jpg' },
rating: 5,
},
];
export default function TestimonialsMarquee() {
const t = useTranslations('landing.testimonials');
return (
<section className="py-20 sm:py-32 bg-white dark:bg-zinc-950 overflow-hidden">
<div className="container mx-auto px-4 mb-12">
<h2 className="text-3xl sm:text-4xl font-bold text-center mb-4">
{t('title')}
</h2>
<p className="text-zinc-600 dark:text-zinc-400 text-center text-lg">
{t('subtitle')}
</p>
</div>
{/* Marquee container */}
<div className="relative">
<div className="flex animate-marquee gap-6">
{testimonials.map((testimonial, index) => (
<div
key={index}
className="flex-shrink-0 w-[400px] bg-zinc-50 dark:bg-zinc-900 rounded-2xl p-6 border border-zinc-200 dark:border-zinc-800"
>
<QuoteIcon className="w-8 h-8 text-zinc-300 dark:text-zinc-700 mb-4" />
<p className="text-zinc-700 dark:text-zinc-300 mb-4">
"{t(testimonial.quoteKey)}"
</p>
<div className="flex items-center gap-1 mb-4">
{Array.from({ length: 5 }).map((_, i) => (
<StarIcon
key={i}
className="w-4 h-4 text-yellow-500 fill-yellow-500"
/>
))}
</div>
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-zinc-300 dark:bg-zinc-700 relative overflow-hidden">
<Image
src={testimonial.author.avatar}
alt={t(testimonial.author.nameKey)}
fill
className="object-cover"
/>
</div>
<div>
<p className="font-semibold text-sm">
{t(testimonial.author.nameKey)}
</p>
<p className="text-zinc-600 dark:text-zinc-400 text-xs">
{t(testimonial.author.roleKey)}
</p>
</div>
</div>
</div>
))}
</div>
</div>
<style jsx>{`
@keyframes marquee {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-50%);
}
}
.animate-marquee {
animation: marquee 30s linear infinite;
}
.animate-marquee:hover {
animation-play-state: paused;
}
`}</style>
</section>
);
}
Best for: Many testimonials, social proof focus, modern designs
Variant 3: Featured (Large)
One large featured testimonial.
// src/components/landing/TestimonialsFeatured.tsx
'use client';
import { useTranslations } from 'next-intl';
import Image from 'next/image';
import { useScrollAnimation } from '@/hooks';
import { StarIcon, QuoteIcon } from '@/components/icons';
export default function TestimonialsFeatured() {
const t = useTranslations('landing.testimonials');
const { elementRef, isVisible } = useScrollAnimation();
return (
<section className="py-20 sm:py-32 bg-gradient-to-br from-blue-50 to-purple-50 dark:from-zinc-900 dark:to-zinc-950">
<div className="container mx-auto px-4">
<div
ref={elementRef as React.RefObject<HTMLDivElement>}
className={`scroll-fade-in ${isVisible ? 'visible' : ''}`}
>
<div className="max-w-4xl mx-auto">
{/* Large quote icon */}
<QuoteIcon className="w-16 h-16 text-blue-500/30 mx-auto mb-8" />
{/* Quote */}
<blockquote className="text-2xl sm:text-3xl lg:text-4xl font-medium text-center text-zinc-900 dark:text-white mb-12 leading-relaxed">
"{t('t1.quote')}"
</blockquote>
{/* Rating */}
<div className="flex items-center justify-center gap-1 mb-8">
{Array.from({ length: 5 }).map((_, i) => (
<StarIcon
key={i}
className="w-6 h-6 text-yellow-500 fill-yellow-500"
/>
))}
</div>
{/* Author */}
<div className="flex flex-col items-center">
<div className="w-20 h-20 rounded-full bg-zinc-200 dark:bg-zinc-800 relative overflow-hidden mb-4">
<Image
src="/images/testimonials/avatar1.jpg"
alt={t('t1.name')}
fill
className="object-cover"
/>
</div>
<p className="font-bold text-xl mb-1">{t('t1.name')}</p>
<p className="text-zinc-600 dark:text-zinc-400">
{t('t1.role')} at {t('t1.company')}
</p>
</div>
{/* Logo cloud */}
<div className="mt-16 pt-16 border-t border-zinc-200 dark:border-zinc-800">
<p className="text-center text-sm text-zinc-600 dark:text-zinc-400 mb-8">
Trusted by leading companies
</p>
<div className="flex items-center justify-center gap-12 flex-wrap opacity-50">
{/* Add company logos here */}
</div>
</div>
</div>
</div>
</div>
</section>
);
}
Best for: Strong testimonials, enterprise clients, credibility focus
Choosing a Variant
| Variant | Use When |
|---|---|
| Grid | 3-6 testimonials, balanced display |
| Marquee | Many testimonials, want continuous motion |
| Featured | One very strong testimonial, enterprise focus |