DocumentationTestimonials Card

Testimonials Card

A stunning 3D animated testimonials carousel with stacked cards, smooth transitions, and beautiful hover effects.

1 / 4
Mountain Landscape
Abstract Art
Nature Photography
Digital Creation

Mountain Landscape

Beautiful mountain scenery with snow-capped peaks.

Install using CLI

npx shadcn@latest add "https://vengeance-ui.vercel.app/r/testimonials-card.json"

Install Manually

1

Install dependencies

npm install clsx tailwind-merge framer-motion lucide-react
2

Add util file

lib/utils.ts

import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
3

Copy the source code

Copy the code below and paste it into components/ui/testimonials-card.tsx

"use client";
import React, { useState, useMemo } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { cn } from "@/lib/utils";
import { ArrowLeft, ArrowRight } from "lucide-react";
interface TestimonialItem {
id: string | number;
title: string;
description: string;
image: string;
}
interface TestimonialsCardProps {
items: TestimonialItem[];
className?: string;
width?: number;
showNavigation?: boolean;
showCounter?: boolean;
autoPlay?: boolean;
autoPlayInterval?: number;
}
export function TestimonialsCard({
items,
className,
width = 400,
showNavigation = true,
showCounter = true,
autoPlay = false,
autoPlayInterval = 3000,
}: TestimonialsCardProps) {
const [activeIndex, setActiveIndex] = useState(0);
const [direction, setDirection] = useState(1);
const activeItem = items[activeIndex];
React.useEffect(() => {
if (!autoPlay || items.length <= 1) return;
const interval = setInterval(() => {
setDirection(1);
setActiveIndex((prev) => (prev + 1) % items.length);
}, autoPlayInterval);
return () => clearInterval(interval);
}, [autoPlay, autoPlayInterval, items.length]);
const handleNext = () => {
if (activeIndex < items.length - 1) {
setDirection(1);
setActiveIndex(activeIndex + 1);
}
};
const handlePrev = () => {
if (activeIndex > 0) {
setDirection(-1);
setActiveIndex(activeIndex - 1);
}
};
const rotations = useMemo(() => [4, -2, -9, 7], []);
if (!items || items.length === 0) return null;
return (
<div className={cn("flex items-center justify-center p-8", className)}>
<div
className="relative grid grid-cols-[1fr_1fr] grid-rows-[auto_auto_auto] gap-x-8 gap-y-2 w-full"
style={{ perspective: "1400px", maxWidth: `${width}px` }}
>
{showCounter && (
<div className="col-start-2 row-start-1 text-right font-mono text-sm text-neutral-500">
{activeIndex + 1} / {items.length}
</div>
)}
<div className="col-start-1 row-start-1 row-span-3 relative w-full aspect-square">
<AnimatePresence custom={direction}>
{items.map((item, index) => {
const isActive = index === activeIndex;
const offset = index - activeIndex;
return (
<motion.div
key={item.id}
className="absolute inset-0 w-full h-full overflow-hidden border-[6px] bg-neutral-200 dark:bg-neutral-800 border-white dark:border-neutral-700 shadow-2xl rounded-lg"
initial={{
x: offset * 15, y: Math.abs(offset) * 6,
z: -150 * Math.abs(offset),
scale: 0.85 - Math.abs(offset) * 0.04,
rotateZ: rotations[index % 4],
opacity: isActive ? 1 : 0.5,
zIndex: 10 - Math.abs(offset),
}}
animate={isActive ? {
x: [offset * 15, direction === 1 ? -200 : 200, 0],
y: [Math.abs(offset) * 6, 0, 0],
z: [-200, 150, 250],
scale: [0.85, 1.05, 1],
rotateZ: [rotations[index % 4], -5, 0],
opacity: 1, zIndex: 100,
} : {
x: offset * 15, y: Math.abs(offset) * 6,
z: -150 * Math.abs(offset),
rotateZ: rotations[index % 4],
scale: 0.85 - Math.abs(offset) * 0.04,
opacity: 0.55, zIndex: 10 - Math.abs(offset),
}}
transition={{ duration: 0.75, ease: [0.22, 1, 0.36, 1] }}
>
<img src={item.image} alt={item.title} className="w-full h-full object-cover" />
</motion.div>
);
})}
</AnimatePresence>
</div>
<div className="col-start-2 row-start-2 flex flex-col justify-center min-h-[120px]">
<AnimatePresence mode="wait">
<motion.div key={activeItem.id}
initial={{ opacity: 0, y: 25 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -25 }}
transition={{ duration: 0.35 }}
>
<h3 className="text-xl font-bold dark:text-white">{activeItem.title}</h3>
<p className="text-sm text-neutral-600 dark:text-neutral-400 mt-2">{activeItem.description}</p>
</motion.div>
</AnimatePresence>
</div>
{showNavigation && items.length > 1 && (
<div className="col-start-2 row-start-3 flex gap-2 mt-4">
<button disabled={activeIndex === 0} onClick={handlePrev}
className={cn("flex items-center justify-center w-10 h-10 rounded-full border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-800 transition-all",
activeIndex === 0 ? "opacity-50 cursor-not-allowed" : "hover:bg-neutral-100 dark:hover:bg-neutral-700 hover:scale-105"
)}>
<ArrowLeft className="w-4 h-4 text-neutral-700 dark:text-neutral-300" />
</button>
<button disabled={activeIndex === items.length - 1} onClick={handleNext}
className={cn("flex items-center justify-center w-10 h-10 rounded-full border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-800 transition-all",
activeIndex === items.length - 1 ? "opacity-50 cursor-not-allowed" : "hover:bg-neutral-100 dark:hover:bg-neutral-700 hover:scale-105"
)}>
<ArrowRight className="w-4 h-4 text-neutral-700 dark:text-neutral-300" />
</button>
</div>
)}
</div>
</div>
);
}
export default TestimonialsCard;

Usage

1import { TestimonialsCard } from "@/components/ui/testimonials-card"
2
3const testimonials = [
4{
5 id: 1,
6 title: "Sarah Wilson",
7 description: "Incredible product! It exceeded all my expectations.",
8 image: "/testimonial-1.jpg",
9},
10{
11 id: 2,
12 title: "David Chen",
13 description: "The best investment I've made this year.",
14 image: "/testimonial-2.jpg",
15},
16];
17
18export function Example() {
19return <TestimonialsCard items={testimonials} />;
20}

Examples

With Auto-Play

1 / 4
Mountain Landscape
Abstract Art
Nature Photography
Digital Creation

Mountain Landscape

Beautiful mountain scenery with snow-capped peaks.

Without Navigation

1 / 4
Mountain Landscape
Abstract Art
Nature Photography
Digital Creation

Mountain Landscape

Beautiful mountain scenery with snow-capped peaks.

Custom Width

1 / 4
Mountain Landscape
Abstract Art
Nature Photography
Digital Creation

Mountain Landscape

Beautiful mountain scenery with snow-capped peaks.

Props

Prop NameTypeDefaultDescription
itemsTestimonialItem[]-Array of testimonial items with id, title, description, and image.
classNamestring-Additional CSS classes for the container.
widthnumber400Maximum width of the component in pixels.
showNavigationbooleantrueWhether to show navigation arrows.
showCounterbooleantrueWhether to show the item counter.
autoPlaybooleanfalseEnable automatic testimonial transitions.
autoPlayIntervalnumber3000Auto-play interval in milliseconds.

TestimonialItem Interface

1interface TestimonialItem {
2id: string | number; // Unique identifier
3title: string; // Person name or title
4description: string; // Testimonial text
5image: string; // Avatar/image URL
6}

Features

3D Animations: Stunning 3D stacked card effect with smooth transitions

Navigation Controls: Built-in previous/next buttons with hover effects

Auto-Play: Optional automatic cycling through testimonials

Counter Display: Shows current position (e.g., “1 / 4”)

Dark Mode Support: Fully adapts to light and dark themes

Responsive: Adjusts to container width

Accessible: Includes aria-labels for screen readers

Performance Optimized: Uses useMemo and efficient re-renders