Card Patterns

Build versatile card components for content display, stats, and interactive elements.

Overview#

Cards are fundamental building blocks for organizing content. This pattern covers:

  • Basic card structure
  • Interactive cards with hover effects
  • Stats and metrics cards
  • Feature cards
  • Pricing cards
  • User profile cards

Basic Card#

A composable card component with header, content, and footer slots.

1// components/ui/Card.tsx 2import { cn } from '@/lib/utils' 3 4interface CardProps { 5 children: React.ReactNode 6 className?: string 7} 8 9export function Card({ children, className = '' }: CardProps) { 10 return ( 11 <div className={cn('rounded-lg border bg-white p-6 shadow-sm', className)}> 12 {children} 13 </div> 14 ) 15} 16 17export function CardHeader({ children, className }: { children: React.ReactNode; className?: string }) { 18 return <div className={cn('mb-4', className)}>{children}</div> 19} 20 21export function CardTitle({ children, className }: { children: React.ReactNode; className?: string }) { 22 return <h3 className={cn('text-lg font-semibold', className)}>{children}</h3> 23} 24 25export function CardDescription({ children, className }: { children: React.ReactNode; className?: string }) { 26 return <p className={cn('text-sm text-gray-500', className)}>{children}</p> 27} 28 29export function CardContent({ children, className }: { children: React.ReactNode; className?: string }) { 30 return <div className={className}>{children}</div> 31} 32 33export function CardFooter({ children, className }: { children: React.ReactNode; className?: string }) { 34 return <div className={cn('mt-4 flex items-center gap-2', className)}>{children}</div> 35} 36 37// Usage 38<Card> 39 <CardHeader> 40 <CardTitle>Card Title</CardTitle> 41 <CardDescription>Card description here</CardDescription> 42 </CardHeader> 43 <CardContent> 44 <p>Card content</p> 45 </CardContent> 46 <CardFooter> 47 <button>Action</button> 48 </CardFooter> 49</Card>

Interactive Card#

A card that links to another page with hover effects.

1// components/ui/InteractiveCard.tsx 2import Link from 'next/link' 3 4interface Props { 5 href: string 6 title: string 7 description: string 8 icon?: React.ReactNode 9} 10 11export function InteractiveCard({ href, title, description, icon }: Props) { 12 return ( 13 <Link 14 href={href} 15 className="group block rounded-lg border p-6 transition-all hover:border-gray-300 hover:shadow-md" 16 > 17 {icon && ( 18 <div className="mb-4 inline-block rounded-lg bg-gray-100 p-3 group-hover:bg-gray-200"> 19 {icon} 20 </div> 21 )} 22 <h3 className="font-semibold group-hover:text-blue-600">{title}</h3> 23 <p className="mt-2 text-sm text-gray-500">{description}</p> 24 </Link> 25 ) 26}

Stats Card#

Display metrics with change indicators.

1// components/cards/StatsCard.tsx 2import { ArrowUp, ArrowDown } from 'lucide-react' 3 4interface Props { 5 title: string 6 value: string | number 7 change?: number 8 changeLabel?: string 9 icon?: React.ReactNode 10} 11 12export function StatsCard({ title, value, change, changeLabel, icon }: Props) { 13 const isPositive = change && change > 0 14 15 return ( 16 <div className="rounded-lg border bg-white p-6"> 17 <div className="flex items-center justify-between"> 18 <p className="text-sm font-medium text-gray-500">{title}</p> 19 {icon && <div className="text-gray-400">{icon}</div>} 20 </div> 21 22 <p className="mt-2 text-3xl font-bold">{value}</p> 23 24 {change !== undefined && ( 25 <div className="mt-2 flex items-center gap-1"> 26 <span className={`flex items-center text-sm ${ 27 isPositive ? 'text-green-600' : 'text-red-600' 28 }`}> 29 {isPositive ? <ArrowUp className="h-4 w-4" /> : <ArrowDown className="h-4 w-4" />} 30 {Math.abs(change)}% 31 </span> 32 {changeLabel && ( 33 <span className="text-sm text-gray-500">{changeLabel}</span> 34 )} 35 </div> 36 )} 37 </div> 38 ) 39} 40 41// Usage - Dashboard stats grid 42<div className="grid gap-4 md:grid-cols-4"> 43 <StatsCard title="Revenue" value="$45,231" change={12.5} changeLabel="from last month" /> 44 <StatsCard title="Users" value="2,345" change={-3.2} changeLabel="from last month" /> 45 <StatsCard title="Orders" value="1,234" change={8.1} changeLabel="from last month" /> 46 <StatsCard title="Conversion" value="3.2%" change={1.2} changeLabel="from last month" /> 47</div>

Feature Card#

Highlight product features with icons and bullet points.

1// components/cards/FeatureCard.tsx 2import { Check } from 'lucide-react' 3 4interface Props { 5 icon: React.ReactNode 6 title: string 7 description: string 8 features: string[] 9} 10 11export function FeatureCard({ icon, title, description, features }: Props) { 12 return ( 13 <div className="rounded-xl border bg-white p-8"> 14 <div className="mb-4 inline-block rounded-lg bg-blue-100 p-3 text-blue-600"> 15 {icon} 16 </div> 17 18 <h3 className="mb-2 text-xl font-semibold">{title}</h3> 19 <p className="mb-4 text-gray-600">{description}</p> 20 21 <ul className="space-y-2"> 22 {features.map((feature, i) => ( 23 <li key={i} className="flex items-center gap-2 text-sm"> 24 <Check className="h-4 w-4 text-green-500" /> 25 {feature} 26 </li> 27 ))} 28 </ul> 29 </div> 30 ) 31}

Pricing Card#

Display pricing tiers with highlighted recommended plan.

1// components/cards/PricingCard.tsx 2import { Check } from 'lucide-react' 3 4interface Props { 5 name: string 6 price: number 7 period?: string 8 description: string 9 features: string[] 10 highlighted?: boolean 11 ctaText?: string 12 onSelect: () => void 13} 14 15export function PricingCard({ 16 name, 17 price, 18 period = '/month', 19 description, 20 features, 21 highlighted = false, 22 ctaText = 'Get Started', 23 onSelect 24}: Props) { 25 return ( 26 <div className={`relative rounded-2xl p-8 ${ 27 highlighted 28 ? 'border-2 border-blue-600 bg-white shadow-xl' 29 : 'border bg-white' 30 }`}> 31 {highlighted && ( 32 <div className="absolute -top-3 left-1/2 -translate-x-1/2 rounded-full bg-blue-600 px-3 py-1 text-xs font-medium text-white"> 33 Most Popular 34 </div> 35 )} 36 37 <h3 className="text-lg font-semibold">{name}</h3> 38 <p className="mt-2 text-sm text-gray-500">{description}</p> 39 40 <div className="my-6"> 41 <span className="text-4xl font-bold">${price}</span> 42 <span className="text-gray-500">{period}</span> 43 </div> 44 45 <ul className="mb-8 space-y-3"> 46 {features.map((feature, i) => ( 47 <li key={i} className="flex items-center gap-2 text-sm"> 48 <Check className="h-5 w-5 text-green-500" /> 49 {feature} 50 </li> 51 ))} 52 </ul> 53 54 <button 55 onClick={onSelect} 56 className={`w-full rounded-lg py-3 font-medium ${ 57 highlighted 58 ? 'bg-blue-600 text-white hover:bg-blue-700' 59 : 'bg-gray-100 text-gray-900 hover:bg-gray-200' 60 }`} 61 > 62 {ctaText} 63 </button> 64 </div> 65 ) 66}

User Card#

Display user information with avatar and actions.

1// components/cards/UserCard.tsx 2import Image from 'next/image' 3 4interface Props { 5 user: { 6 name: string 7 email: string 8 avatar?: string 9 role: string 10 } 11 actions?: React.ReactNode 12} 13 14export function UserCard({ user, actions }: Props) { 15 return ( 16 <div className="flex items-center justify-between rounded-lg border p-4"> 17 <div className="flex items-center gap-4"> 18 <div className="relative h-12 w-12 overflow-hidden rounded-full bg-gray-200"> 19 {user.avatar ? ( 20 <Image src={user.avatar} alt={user.name} fill className="object-cover" /> 21 ) : ( 22 <div className="flex h-full w-full items-center justify-center text-lg font-medium text-gray-600"> 23 {user.name.charAt(0)} 24 </div> 25 )} 26 </div> 27 28 <div> 29 <p className="font-medium">{user.name}</p> 30 <p className="text-sm text-gray-500">{user.email}</p> 31 </div> 32 </div> 33 34 <div className="flex items-center gap-4"> 35 <span className="rounded-full bg-gray-100 px-3 py-1 text-xs font-medium"> 36 {user.role} 37 </span> 38 {actions} 39 </div> 40 </div> 41 ) 42}

Image Card#

Cards with featured images.

1// components/cards/ImageCard.tsx 2import Image from 'next/image' 3import Link from 'next/link' 4 5interface Props { 6 href: string 7 image: string 8 title: string 9 description: string 10 date?: string 11 author?: string 12} 13 14export function ImageCard({ href, image, title, description, date, author }: Props) { 15 return ( 16 <Link href={href} className="group block overflow-hidden rounded-lg border"> 17 <div className="relative aspect-video overflow-hidden"> 18 <Image 19 src={image} 20 alt={title} 21 fill 22 className="object-cover transition-transform group-hover:scale-105" 23 /> 24 </div> 25 <div className="p-4"> 26 <h3 className="font-semibold group-hover:text-blue-600">{title}</h3> 27 <p className="mt-2 text-sm text-gray-500 line-clamp-2">{description}</p> 28 {(date || author) && ( 29 <div className="mt-4 flex items-center gap-2 text-xs text-gray-400"> 30 {date && <span>{date}</span>} 31 {date && author && <span>-</span>} 32 {author && <span>{author}</span>} 33 </div> 34 )} 35 </div> 36 </Link> 37 ) 38}

Best Practices#

  1. Keep cards scannable - Use clear hierarchy with headings and spacing
  2. Use consistent sizes - Cards in a grid should have uniform heights
  3. Add hover states - Interactive cards should provide visual feedback
  4. Consider card density - Don't overcrowd cards with too much information
  5. Use shadows sparingly - Subtle shadows work better than heavy ones
  • Layouts - Grid layouts for cards
  • Tables - Alternative to cards for data display
  • Navigation - Card-based navigation