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#
- Keep cards scannable - Use clear hierarchy with headings and spacing
- Use consistent sizes - Cards in a grid should have uniform heights
- Add hover states - Interactive cards should provide visual feedback
- Consider card density - Don't overcrowd cards with too much information
- Use shadows sparingly - Subtle shadows work better than heavy ones
Related Patterns#
- Layouts - Grid layouts for cards
- Tables - Alternative to cards for data display
- Navigation - Card-based navigation