Testimonials

Customer testimonials with ratings.

Default Usage

import { Testimonials } from '@/components/landing';

<Testimonials />

Location: src/components/landing/Testimonials.tsx

Features

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