Trusted by industry leaders
See why companies choose us
Boosted our sales by 60%
"The lead management and pipeline features are outstanding. Our sales team adopted it immediately, and we're closing deals faster than ever. The mobile app is also a huge plus for our field team."
Developer-friendly and powerful
"Clean API, excellent documentation, and great developer experience. We integrated it into our stack in less than a day. The webhook system is reliable and the error handling is top-notch."
Our customers love it
"We use this to manage all our customer interactions and it's been phenomenal. The automation saves us time, and our customer satisfaction scores have improved by 25%. Highly recommended!"
Clear ROI within 3 months
"The pricing is transparent, the value is clear, and we saw positive ROI within the first quarter. The financial reporting features have also helped us make better business decisions."
Installation
Install dependencies
npm install framer-motion lucide-reactCopy and paste the following code into your project.
"use client";
import { motion } from "framer-motion";
import { Star, ThumbsUp } from "lucide-react";
interface Review {
id: string;
author: {
name: string;
role: string;
company: string;
avatar?: string;
};
rating: number;
title: string;
content: string;
date: string;
verified?: boolean;
helpful?: number;
platform?: string;
}
interface ReviewCardsProps {
title?: string;
description?: string;
reviews: Review[];
columns?: 2 | 3;
className?: string;
}
export function ReviewCards({
title = "What our customers are saying",
description = "Real reviews from real users",
reviews,
columns = 3,
className = "",
}: ReviewCardsProps) {
const gridCols =
columns === 2 ? "md:grid-cols-2" : "md:grid-cols-2 lg:grid-cols-3";
return (
<section className={`w-full bg-white py-20 dark:bg-black ${className}`}>
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="mb-16 text-center">
<h2 className="mb-4 text-4xl font-bold text-zinc-900 dark:text-white sm:text-5xl">
{title}
</h2>
<p className="mx-auto max-w-2xl text-lg text-zinc-600 dark:text-zinc-400">
{description}
</p>
</div>
{/* Reviews Grid */}
<div className={`grid gap-6 ${gridCols}`}>
{reviews.map((review, index) => (
<motion.article
key={review.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
className="group flex flex-col rounded-2xl border border-zinc-200 bg-white p-8 transition-all duration-300 hover:border-zinc-300 hover:shadow-lg dark:border-zinc-800 dark:bg-zinc-950 dark:hover:border-zinc-700"
>
<div className="mb-6 flex items-start justify-between">
<div className="flex gap-0.5">
{Array.from({ length: 5 }).map((_, i) => (
<Star
key={i}
className={`h-4 w-4 ${
i < review.rating
? "fill-yellow-400 text-yellow-400"
: "fill-zinc-200 text-zinc-200 dark:fill-zinc-800 dark:text-zinc-800"
}`}
/>
))}
</div>
{review.platform && (
<div className="rounded-full bg-zinc-100 px-2.5 py-1 text-xs font-medium text-zinc-700 dark:bg-zinc-900 dark:text-zinc-300">
{review.platform}
</div>
)}
</div>
<h3 className="mb-3 text-lg font-bold text-zinc-900 dark:text-white">
{review.title}
</h3>
<p className="mb-6 flex-1 text-base leading-relaxed text-zinc-700 dark:text-zinc-300">
"{review.content}"
</p>
<div className="space-y-4 border-t border-zinc-100 pt-6 dark:border-zinc-800">
<div className="flex items-center gap-3">
<div className="relative h-10 w-10 flex-shrink-0 overflow-hidden rounded-full bg-zinc-100 dark:bg-zinc-800">
{review.author.avatar ? (
<img
src={review.author.avatar}
alt={review.author.name}
className="h-full w-full object-cover"
/>
) : (
<div className="flex h-full w-full items-center justify-center text-sm font-semibold text-zinc-600 dark:text-zinc-400">
{review.author.name
.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase()
.slice(0, 2)}
</div>
)}
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<div className="truncate font-semibold text-zinc-900 dark:text-white">
{review.author.name}
</div>
{review.verified && (
<div className="flex items-center gap-1 rounded-full bg-emerald-50 px-2 py-0.5 text-xs font-medium text-emerald-700 dark:bg-emerald-950 dark:text-emerald-400">
<svg
className="h-3 w-3"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clipRule="evenodd"
/>
</svg>
Verified
</div>
)}
</div>
<div className="truncate text-sm text-zinc-600 dark:text-zinc-400">
{review.author.role} · {review.author.company}
</div>
</div>
</div>
<div className="flex items-center justify-between text-sm">
<div className="text-zinc-500 dark:text-zinc-500">
{review.date}
</div>
{review.helpful !== undefined && (
<div className="flex items-center gap-1.5 text-zinc-600 dark:text-zinc-400">
<ThumbsUp className="h-3.5 w-3.5" />
<span className="text-xs font-medium">
{review.helpful} helpful
</span>
</div>
)}
</div>
</div>
</motion.article>
))}
</div>
<div className="mt-16 rounded-2xl border border-zinc-200 bg-zinc-50 p-8 dark:border-zinc-800 dark:bg-zinc-900">
<div className="grid gap-8 sm:grid-cols-3">
<div className="text-center">
<div className="mb-2 text-4xl font-bold text-zinc-900 dark:text-white">
4.9/5
</div>
<div className="text-sm text-zinc-600 dark:text-zinc-400">
Average Rating
</div>
</div>
<div className="text-center">
<div className="mb-2 text-4xl font-bold text-zinc-900 dark:text-white">
2,500+
</div>
<div className="text-sm text-zinc-600 dark:text-zinc-400">
Total Reviews
</div>
</div>
<div className="text-center">
<div className="mb-2 text-4xl font-bold text-zinc-900 dark:text-white">
98%
</div>
<div className="text-sm text-zinc-600 dark:text-zinc-400">
Recommend Us
</div>
</div>
</div>
</div>
</div>
</section>
);
}Features
- ✅ Star ratings - Visual 5-star rating display
- ✅ Verified badges - Show verified customer reviews
- ✅ Platform badges - Display review source (G2, Capterra, etc.)
- ✅ Author info - Name, role, company with avatar
- ✅ Helpful counts - Show how many found review helpful
- ✅ Review dates - Display when review was posted
- ✅ Flexible columns - Choose 2 or 3 column layouts
- ✅ Bottom stats - Aggregate review statistics
- ✅ Hover effects - Smooth border and shadow animations
- ✅ Light/dark mode - Full theme support
- ✅ TypeScript support - Full type safety
Usage
Basic Usage (3 Columns)
import ReviewCards from "@/components/ui/review-cards";
const reviews = [
{
id: "1",
author: {
name: "Sarah Mitchell",
role: "Product Manager",
company: "TechFlow Inc",
},
rating: 5,
title: "Game changer for our team",
content: "This platform has completely transformed...",
date: "2 days ago",
verified: true,
helpful: 24,
platform: "G2",
},
// ... more reviews
];
export default function Page() {
return <ReviewCards reviews={reviews} />;
}2 Column Layout
<ReviewCards reviews={reviews} columns={2} />With Custom Title
<ReviewCards
title="Customer Reviews"
description="Real feedback from real users"
reviews={reviews}
/>Props
ReviewCardsProps
| Prop | Type | Default | Description |
|---|---|---|---|
reviews | Review[] | Required | Array of review objects |
title | string | "What our customers..." | Section title |
description | string | "Real reviews from..." | Section description |
columns | 2 | 3 | 3 | Number of columns |
className | string | "" | Additional CSS classes |
Review
| Prop | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Unique identifier |
author | Author | Yes | Author information object |
rating | number | Yes | Rating (1-5) |
title | string | Yes | Review title |
content | string | Yes | Review content |
date | string | Yes | Review date |
verified | boolean | No | Show verified badge |
helpful | number | No | Helpful count |
platform | string | No | Review platform name |
Author
| Prop | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Author name |
role | string | Yes | Author job title |
company | string | Yes | Author company |
avatar | string | No | Avatar image URL |
TypeScript Interface
interface Review {
id: string;
author: {
name: string;
role: string;
company: string;
avatar?: string;
};
rating: number;
title: string;
content: string;
date: string;
verified?: boolean;
helpful?: number;
platform?: string;
}
interface ReviewCardsProps {
title?: string;
description?: string;
reviews: Review[];
columns?: 2 | 3;
className?: string;
}Grid Layouts
3 Column (Default)
- Mobile: 1 column
- Tablet (md+): 2 columns
- Desktop (lg+): 3 columns
2 Column
- Mobile: 1 column
- Tablet (md+): 2 columns
Features Breakdown
Star Ratings
- Visual 5-star display
- Filled stars in yellow
- Empty stars in gray
- Supports 1-5 ratings
Verified Badge
- Green badge with checkmark
- Shows "Verified" text
- Optional per review
- Builds trust and credibility
Platform Badge
- Gray badge in top-right
- Shows review source
- Examples: G2, Capterra, Trustpilot
- Optional per review
Author Section
- Avatar with initials fallback
- Name with verified badge
- Role and company
- Truncates long text
Helpful Count
- Thumbs up icon
- Shows helpful count
- Bottom-right of card
- Optional per review
Review Date
- Relative dates work best
- Examples: "2 days ago", "1 week ago"
- Bottom-left of card
- Always displayed
Styling
- Background: White/Black with theme support
- Cards: White/Zinc-950 with borders
- Text: Zinc colors with dark mode
- Borders: Zinc-200/800 default, zinc-300/700 on hover
- Stars: Yellow-400 filled, Zinc-200/800 empty
- Verified Badge: Emerald-50/950 with border
- Platform Badge: Zinc-100/900 background
- Avatar: Zinc-100/800 background
- Stats Card: Zinc-50/900 background
Use Cases
Perfect for:
- SaaS landing pages
- Product pages
- Review sections
- Social proof displays
- Case study pages
- Testimonial pages
- Trust building sections
- Conversion optimization
Best Practices
- Number of reviews - Use 3, 6, or 9 for best layout
- Content length - Keep reviews 2-4 sentences (50-100 words)
- Ratings - Mix of 4-5 star reviews for authenticity
- Verified badges - Use for 50-70% of reviews
- Platforms - Show diverse review sources
- Dates - Use recent dates (within 2-4 weeks)
- Helpful counts - Use realistic numbers (10-30)
- Titles - Keep concise and descriptive
- Authors - Use real names and companies
- Mix ratings - Include some 4-star reviews
Customization
Change Star Color
// Modify the star className
className={`h-4 w-4 ${
i < review.rating
? "fill-blue-400 text-blue-400" // Changed from yellow to blue
: "fill-zinc-200 text-zinc-200 dark:fill-zinc-800 dark:text-zinc-800"
}`}Remove Platform Badges
// Simply don't include platform property in review objects
const reviews = [
{
id: "1",
// ... other props
// No platform property
},
];Add Review Images
// Add to Review interface
interface Review {
// ... existing props
images?: string[];
}
// In the component, add after content
{
review.images && review.images.length > 0 && (
<div className="mb-4 flex gap-2">
{review.images.map((img, idx) => (
<img
key={idx}
src={img}
alt={`Review image ${idx + 1}`}
className="h-20 w-20 rounded-lg object-cover"
/>
))}
</div>
);
}Custom Stats
// Modify the bottom stats section
<div className="grid gap-8 sm:grid-cols-4">
<div className="text-center">
<div className="mb-2 text-4xl font-bold">5.0/5</div>
<div className="text-sm">Your Custom Stat</div>
</div>
{/* ... more stats */}
</div>Add Response from Company
// Add to Review interface
interface Review {
// ... existing props
response?: {
author: string;
content: string;
date: string;
};
}
// In the component, add after footer
{
review.response && (
<div className="mt-4 rounded-lg border-l-4 border-blue-500 bg-blue-50 p-4 dark:bg-blue-950">
<div className="mb-1 text-sm font-semibold text-blue-900 dark:text-blue-100">
Response from {review.response.author}
</div>
<p className="text-sm text-blue-800 dark:text-blue-200">
{review.response.content}
</p>
<div className="mt-2 text-xs text-blue-600 dark:text-blue-400">
{review.response.date}
</div>
</div>
);
}Change Card Height
// The cards use flex-col with flex-1 on content
// To set minimum height:
className = "... min-h-[400px]";Animation Details
- Staggered load - Cards appear sequentially (0.1s delay)
- Fade in - Opacity 0 to 1
- Slide up - 20px vertical movement
- Hover border - Border color changes
- Hover shadow - Shadow increases
- All transitions - 300ms duration
Accessibility
- Semantic HTML - Uses article tag for reviews
- Alt text - Avatar images have descriptive alt text
- Color contrast - WCAG AA compliant
- Keyboard navigation - All interactive elements focusable
- Screen readers - Proper ARIA labels
Performance
- Lazy animations - Only animates visible cards
- Optimized images - Use next/image for avatars
- Minimal re-renders - Static content after load
- GPU acceleration - Uses transform and opacity
Common Patterns
With Section Divider
<ReviewCards
reviews={reviews}
className="border-t border-zinc-200 dark:border-zinc-800"
/>Compact Version
<ReviewCards
title=""
description=""
reviews={reviews}
className="py-12" // Reduced padding
/>Featured Reviews
<ReviewCards
title="Featured Reviews"
description="Handpicked reviews from our top customers"
reviews={featuredReviews}
columns={2}
/>