Card
A raised surface that groups related content and actions about a single subject.
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Progress } from "@/components/ui/progress"
export default function CardDemo() {
return (
<Card className="w-full max-w-sm">
<CardHeader>
<CardTitle>Storage</CardTitle>
<CardDescription>36.2 GB of 50 GB used</CardDescription>
</CardHeader>
<CardContent>
<Progress value={72} aria-label="Storage used" />
</CardContent>
<CardFooter className="justify-end">
<Button size="sm">Upgrade plan</Button>
</CardFooter>
</Card>
)
}Anatomy
- 1ContainerA raised surface with a hairline border that holds one subject. Its size sets the padding and the gaps between parts.
- 2FooterActions for the whole card, on a quiet band below a hairline. It stays at the bottom when cards in a row stretch to equal height.
- 3TitleNames the subject. A description below it adds context.
- 4ContentThe body — anything, padded to line up with the header and footer.
- 5ActionAn optional control in the header's top corner, aligned with the title.
Installation
$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/card.jsonUsage
import {
Card,
CardAction,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"<Card>
<CardHeader>
<CardTitle>Storage</CardTitle>
<CardDescription>36.2 GB of 50 GB used</CardDescription>
</CardHeader>
<CardContent>…</CardContent>
<CardFooter>…</CardFooter>
</Card>Use the full composition. The parts share one spacing variable, so headers, content and footers line up without extra classes.
Examples
Sizes
default has 24px of padding and 20px between sections. sm tightens both to 16px and 12px for dashboards and dense grids. Every part reads the same --card-spacing variable, so you never have to override padding on individual sections.
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
export default function CardSizes() {
return (
<div className="grid w-full max-w-2xl items-start gap-4 sm:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Default</CardTitle>
<CardDescription>
24px padding, 20px between sections.
</CardDescription>
</CardHeader>
<CardContent className="text-muted-foreground">
For primary content: forms, summaries, settings groups.
</CardContent>
</Card>
<Card size="sm">
<CardHeader>
<CardTitle>Small</CardTitle>
<CardDescription>
16px padding, 12px between sections.
</CardDescription>
</CardHeader>
<CardContent className="text-muted-foreground">
For dense dashboards and grids of tiles.
</CardContent>
</Card>
</div>
)
}With an action
CardAction places controls in the top-right corner of the header and keeps the title and description aligned on the left, however long they get. When space runs out, the title wraps before the action is squeezed, and footer actions wrap onto a second line, so nothing is cut off on a phone.
import { MoreHorizontalIcon, UserPlusIcon } from "lucide-react"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
Card,
CardAction,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
const members = [
{
name: "Maya Chen",
email: "maya@northwind.dev",
role: "Owner",
owner: true,
},
{ name: "Jonah Okafor", email: "jonah@northwind.dev", role: "Admin" },
{ name: "Lena Park", email: "lena@northwind.dev", role: "Member" },
]
export default function CardWithAction() {
return (
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle>Team members</CardTitle>
<CardDescription>3 of 5 seats used.</CardDescription>
<CardAction className="flex gap-1">
<Button size="sm" variant="outline">
<UserPlusIcon data-icon="inline-start" />
Invite
</Button>
<Button size="icon-sm" variant="ghost" aria-label="More options">
<MoreHorizontalIcon />
</Button>
</CardAction>
</CardHeader>
<CardContent className="flex flex-col gap-4">
{members.map((member) => (
<div key={member.email} className="flex items-center gap-3">
<Avatar>
<AvatarFallback>
{member.name
.split(" ")
.map((part) => part[0])
.join("")}
</AvatarFallback>
</Avatar>
<div className="flex min-w-0 flex-1 flex-col">
<span className="truncate text-sm font-medium">
{member.name}
</span>
<span className="truncate text-xs text-muted-foreground">
{member.email}
</span>
</div>
<Badge variant={member.owner ? "brand" : "secondary"}>
{member.role}
</Badge>
</div>
))}
</CardContent>
</Card>
)
}Stat
Cards are the natural home for key figures. Keep the label small and quiet, the value large, and state the comparison period next to any change.
import { ArrowDownRightIcon, ArrowUpRightIcon } from "lucide-react"
import { Badge } from "@/components/ui/badge"
import {
Card,
CardContent,
CardDescription,
CardHeader,
} from "@/components/ui/card"
const stats = [
{
label: "Monthly revenue",
value: "$48,210",
delta: "+12.4%",
up: true,
good: true,
},
{
label: "Active users",
value: "8,924",
delta: "+3.1%",
up: true,
good: true,
},
{
label: "Churn rate",
value: "1.8%",
delta: "+0.3 pt",
up: true,
good: false,
},
]
export default function CardStat() {
return (
<div className="grid w-full max-w-3xl gap-4 sm:grid-cols-3">
{stats.map((stat) => {
const Icon = stat.up ? ArrowUpRightIcon : ArrowDownRightIcon
return (
<Card key={stat.label} size="sm">
<CardHeader>
<CardDescription>{stat.label}</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-1.5">
<div className="flex items-center justify-between gap-2">
{/* Proportional figures: tabular digits look loose at display size. */}
<span className="text-heading-lg">{stat.value}</span>
<Badge variant={stat.good ? "success" : "destructive"}>
<Icon data-icon="inline-start" />
{stat.delta}
</Badge>
</div>
<span className="text-xs text-muted-foreground">
vs. last month
</span>
</CardContent>
</Card>
)
})}
</div>
)
}Pricing
import { CheckIcon } from "lucide-react"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
Card,
CardAction,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
const features = [
"Unlimited projects",
"Up to 20 collaborators",
"Audit log and SSO",
"Priority support, 4h response",
]
export default function CardPricing() {
return (
<Card className="w-full max-w-sm">
<CardHeader>
<CardTitle>Team</CardTitle>
<CardDescription>
For product teams shipping every week.
</CardDescription>
<CardAction>
<Badge variant="brand">Most popular</Badge>
</CardAction>
</CardHeader>
<CardContent className="flex flex-col gap-5">
<p className="flex items-baseline gap-1">
<span className="text-display-sm">$24</span>
<span className="text-sm text-muted-foreground">
per seat / month
</span>
</p>
<ul className="flex flex-col gap-2.5">
{features.map((feature) => (
<li key={feature} className="flex items-center gap-2.5 text-sm">
<CheckIcon
className="size-4 shrink-0 text-brand-11"
aria-hidden="true"
/>
{feature}
</li>
))}
</ul>
</CardContent>
<CardFooter>
<Button className="w-full">Start 14-day trial</Button>
</CardFooter>
</Card>
)
}Settings
A bordered header and the tinted CardFooter strip frame a group of settings. The footer is where the card's commit action lives — separated from the controls it saves.
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import {
Field,
FieldContent,
FieldDescription,
FieldGroup,
FieldLabel,
} from "@/components/ui/field"
import { Switch } from "@/components/ui/switch"
export default function CardSettings() {
return (
<Card className="w-full max-w-lg">
<CardHeader className="border-b">
<CardTitle>Notifications</CardTitle>
<CardDescription>Choose what we email you about.</CardDescription>
</CardHeader>
<CardContent>
<FieldGroup>
<Field orientation="horizontal">
<FieldContent>
<FieldLabel htmlFor="deploys">Deployments</FieldLabel>
<FieldDescription>
When a production deploy succeeds or fails.
</FieldDescription>
</FieldContent>
<Switch id="deploys" defaultChecked />
</Field>
<Field orientation="horizontal">
<FieldContent>
<FieldLabel htmlFor="comments">Comments</FieldLabel>
<FieldDescription>
When someone mentions you in a review.
</FieldDescription>
</FieldContent>
<Switch id="comments" defaultChecked />
</Field>
<Field orientation="horizontal">
<FieldContent>
<FieldLabel htmlFor="digest">Weekly digest</FieldLabel>
<FieldDescription>
A Monday summary of usage and spend.
</FieldDescription>
</FieldContent>
<Switch id="digest" />
</Field>
</FieldGroup>
</CardContent>
<CardFooter className="justify-between">
<p className="text-xs text-muted-foreground">
Changes apply to all projects.
</p>
<Button size="sm">Save preferences</Button>
</CardFooter>
</Card>
)
}With media
Place an <img> as the first child and the card removes its top padding and rounds the image to its corners. Decorative covers get an empty alt.
import { CalendarIcon, MapPinIcon } from "lucide-react"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import cover from "./assets/conference-cover.svg"
export default function CardImage() {
return (
<Card className="w-full max-w-sm">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={cover.src}
alt=""
width={640}
height={360}
className="aspect-[16/9] w-full object-cover"
/>
<CardHeader>
<div className="flex items-center gap-2">
<Badge variant="brand">Conference</Badge>
<Badge variant="outline">Hybrid</Badge>
</div>
<CardTitle className="text-heading-md">
Systems & Craft 2026
</CardTitle>
<CardDescription>
Two days on design systems, tokens and the craft of interface
engineering.
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-2 text-sm text-muted-foreground">
<span className="flex items-center gap-2">
<CalendarIcon className="size-4" aria-hidden="true" />
October 14–15, 2026
</span>
<span className="flex items-center gap-2">
<MapPinIcon className="size-4" aria-hidden="true" />
Kraków and online
</span>
</CardContent>
<CardFooter className="gap-2">
<Button className="flex-1">Get tickets</Button>
<Button variant="outline">Agenda</Button>
</CardFooter>
</Card>
)
}Guidelines
When to use
- To group content and actions about one subject — a project, a plan, a metric, a settings section.
- To present a collection of peer objects in a grid, where each can be scanned and compared.
When not to use
- To add visual separation to a single block of content on a page — use spacing or a Separator.
- For rows of homogeneous data — use a Table or a list of Items.
- For content that needs attention right now — use an Alert or a Dialog.
One subject, one card
Actions
- Put the card's primary action in the
CardFooter, aligned to the end. Secondary actions go before it. - Put object-level actions — Edit, Share, an overflow menu — in
CardAction. - If the whole card navigates, make the title the link and stretch its hit area over the card, instead of wrapping interactive children inside a link.
Elevation
Cards sit at the raised level: a hairline border and no shadow, in both modes. Don't add shadows to make a card stand out — emphasis comes from content and position, not depth. Floating shadows belong to popovers and dialogs.
Accessibility
- Give it a heading when it's a section.
CardTitlerenders a<div>so it fits any document outline. When cards are major sections of a page, put a heading of the right level inside it —<CardTitle><h3>Storage</h3></CardTitle>— and it inherits the title style. - Keep reading order logical.
CardActionis placed visually in the corner but comes after the title and description in the DOM, so screen readers hear what the card is before what you can do with it. - Media needs alternatives. Informative images need
alttext; decorative ones needalt=""oraria-hidden.
API reference
Card
A <div> with the raised surface, border and shared spacing.
CardHeader
A grid that lays out the title, description and an optional CardAction. Add className="border-b" to separate it from the content; its bottom padding follows automatically.
CardTitle
The card's title, set in heading-sm (heading-xs in small cards).
CardDescription
Supporting text under the title in muted-foreground.
CardAction
Controls pinned to the top-right corner of the header, spanning the title and description rows.
CardContent
The main content, padded horizontally to match the header.
CardFooter
A tinted strip separated by a border, for the card's actions and status. It removes the card's bottom padding so the tint reaches the edge.