A surface that groups related content and actions. Four surface styles, three padding sizes, a header with an action slot and optional hairline, an inset surface for previews, and a footer for actions. Server-component safe.
<Card> <CardHeader> <CardTitle>Usage this month</CardTitle> <CardDescription>Resets on the 1st.</CardDescription> </CardHeader> <CardContent>…</CardContent> <CardFooter>…</CardFooter></Card>
Plain elements, no client JavaScript
Every card part is a styled <div> with no React Aria behavior and no
"use client" directive, so cards render in server components. Put
interactive children (buttons, fields, links) inside and they bring their own
behavior.
<Card> {/* surface: variant, size (sets --card-px) */} <CardHeader> {/* grid: title + description, action on the right */} <CardTitle /> {/* a leading svg becomes an icon */} <CardDescription /> <CardAction /> {/* top-right slot: button, badge, menu */} </CardHeader> <CardContent> {/* horizontally padded body */} <CardInset /> {/* optional muted inner surface */} </CardContent> <CardFooter /> {/* row of actions or meta text */}</Card>
Part
Renders
Notes
Card
<div>
Root. Flex column with vertical padding and a gap between parts. Sets the --card-px variable used by the other parts.
CardHeader
<div>
Two-row grid. Switches to two columns when it contains a CardAction. separator draws an inset hairline underneath.
CardTitle
<div>
Medium-weight title. Any direct svg child is sized to 16px and spaced as a leading icon.
CardDescription
<div>
Muted supporting text.
CardAction
<div>
Pinned to the top-right of the header, spanning both rows.
CardContent
<div>
Adds the card's horizontal padding.
CardInset
<div>
A rounded, muted surface for previews, code, media or empty states. Add your own padding.
CardFooter
<div>
Horizontal flex row with the card's horizontal padding.
The parts render as <div> for layout flexibility. CardTitle is not a heading element; if the card title should appear in the document outline, put an <h2>/<h3> inside it.
outline is the default and suits most dashboard surfaces. elevated lifts featured content with a soft shadow, filled is a muted panel with no border for secondary or nested content, and ghost keeps the spacing and slots with no surface at all.
Outline
The default. A hairline border for most dashboard surfaces.
variant="outline"
Elevated
A soft shadow that lifts featured content off the page.
variant="elevated"
Filled
A muted fill with no border, for secondary or nested panels.
variant="filled"
Ghost
No surface at all. Keeps the spacing and slots only.
variant="ghost"
import { Card, CardContent, CardDescription, CardHeader, CardTitle,} from "@/components/ui/card";const variants = [ { variant: "outline", title: "Outline", text: "The default. A hairline border for most dashboard surfaces.", }, { variant: "elevated", title: "Elevated", text: "A soft shadow that lifts featured content off the page.", }, { variant: "filled", title: "Filled", text: "A muted fill with no border, for secondary or nested panels.", }, { variant: "ghost", title: "Ghost", text: "No surface at all. Keeps the spacing and slots only.", },] as const;export default function CardVariants() { return ( <div className="grid w-full max-w-2xl gap-4 sm:grid-cols-2"> {variants.map((v) => ( <Card key={v.variant} variant={v.variant} size="sm"> <CardHeader> <CardTitle>{v.title}</CardTitle> <CardDescription>{v.text}</CardDescription> </CardHeader> <CardContent> <code className="text-muted-foreground text-xs"> variant="{v.variant}" </code> </CardContent> </Card> ))} </div> );}
size controls the vertical padding, the gap between parts and the --card-px horizontal padding every part reads. sm (12px) suits dense grids, md (16px) is the default and lg (24px) suits forms and focused content.
Size sm
12px padding and gap. Dense grids and sidebars.
Size md
16px padding and gap. The default.
Size lg
24px padding and gap. Forms and focused pages.
import { Card, CardContent, CardDescription, CardHeader, CardTitle,} from "@/components/ui/card";const sizes = [ { size: "sm", note: "12px padding and gap. Dense grids and sidebars." }, { size: "md", note: "16px padding and gap. The default." }, { size: "lg", note: "24px padding and gap. Forms and focused pages." },] as const;export default function CardSizes() { return ( <div className="flex w-full max-w-sm flex-col gap-4"> {sizes.map((s) => ( <Card key={s.size} size={s.size}> <CardHeader> <CardTitle>Size {s.size}</CardTitle> <CardDescription>{s.note}</CardDescription> </CardHeader> <CardContent> <div className="h-2 rounded-full bg-muted" /> </CardContent> </Card> ))} </div> );}
Pass separator to CardHeader for an inset hairline aligned with the content. An svg inside CardTitle becomes a leading icon, CardAction holds one quiet action in the top-right corner, and CardInset is the muted surface for the headline figure.
Product health
Health score87
An icon title, an inset divider and one quiet action: the anatomy every widget card shares.
import { GaugeIcon } from "lucide-react";import { Button } from "@/components/ui/button";import { Card, CardAction, CardContent, CardHeader, CardInset, CardTitle,} from "@/components/ui/card";export default function CardWidget() { return ( <Card className="w-full max-w-sm"> <CardHeader separator> <CardTitle> <GaugeIcon /> Product health </CardTitle> <CardAction> <Button variant="outline" size="xs"> Details </Button> </CardAction> </CardHeader> <CardContent className="flex flex-col gap-3"> <CardInset className="flex items-baseline justify-between p-3"> <span className="text-muted-foreground text-xs">Health score</span> <span className="font-semibold text-2xl tabular-nums">87</span> </CardInset> <p className="text-muted-foreground text-xs leading-relaxed"> An icon title, an inset divider and one quiet action: the anatomy every widget card shares. </p> </CardContent> </Card> );}
Add border-b to CardHeader or border-t to CardFooter for full-width dividers; the parts add matching padding on the bordered side automatically. Use this for settings sections where the footer holds the save action.
Workspace name
Shown in the sidebar, invitations and email notifications.
Max. 32 characters.
import { Button } from "@/components/ui/button";import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle,} from "@/components/ui/card";import { TextField } from "@/components/ui/text-field";export default function CardFooterExample() { return ( <Card className="w-full max-w-md"> <CardHeader className="border-b"> <CardTitle>Workspace name</CardTitle> <CardDescription> Shown in the sidebar, invitations and email notifications. </CardDescription> </CardHeader> <CardContent> <TextField aria-label="Workspace name" defaultValue="Northwind Analytics" /> </CardContent> <CardFooter className="justify-between border-t"> <p className="text-muted-foreground text-xs">Max. 32 characters.</p> <Button size="sm">Save</Button> </CardFooter> </Card> );}
For an edge-to-edge cover, remove the top padding with pt-0 and clip the corners with overflow-hidden. The cover can be an image, video or illustration.
Conference
Design Systems Summit 2026
Two days of talks on tokens, accessibility and component APIs.
Nov 12–13, 2026 Lisbon, Portugal
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";export default function CardMedia() { return ( <Card className="w-full max-w-sm overflow-hidden pt-0"> <div className="relative aspect-[2/1] bg-[radial-gradient(circle_at_20%_20%,var(--chart-2),transparent_55%),radial-gradient(circle_at_80%_30%,var(--chart-1),transparent_50%),linear-gradient(135deg,var(--chart-5),var(--chart-4))]"> <Badge variant="solid" color="neutral" shape="pill" className="absolute top-3 left-3" > Conference </Badge> </div> <CardHeader> <CardTitle>Design Systems Summit 2026</CardTitle> <CardDescription> Two days of talks on tokens, accessibility and component APIs. </CardDescription> </CardHeader> <CardContent className="flex flex-col gap-1.5 text-muted-foreground text-xs"> <span className="flex items-center gap-1.5"> <CalendarIcon className="size-3.5" /> Nov 12–13, 2026 </span> <span className="flex items-center gap-1.5"> <MapPinIcon className="size-3.5" /> Lisbon, Portugal </span> </CardContent> <CardFooter> <Button size="sm" className="w-full"> Get tickets </Button> </CardFooter> </Card> );}
isInteractive only adds a pointer cursor and hover feedback; it does not make the card focusable or clickable. Put a real link inside and stretch it over the card with an after:absolute after:inset-0 overlay, so middle-click, "open in new tab" and keyboard focus keep working. has-[a:focus-visible] moves the focus ring to the card.
A card that holds a list of Item rows with Avatars, role badges and a per-row menu button. px-2 on CardContent lets row hover backgrounds reach closer to the edge.
Cards have no role. They're visual grouping only; screen readers read the content in order.
CardTitle renders a <div>. When the card's title is a real section heading, render a heading element inside it (for example <CardTitle><h3>…</h3></CardTitle>) so it shows up in the heading outline.
For a card that navigates, use a real link (see Interactive cards); don't put onClick on the card <div>. Keep other interactive elements outside the stretched link's overlay, or raise them with relative z-10.
isInteractive is purely visual. It doesn't add focus, a role or keyboard handling.
If a card is a meaningful landmark (for example a named region of a dashboard), add role="region" and aria-labelledby pointing at its title.