Card
The one container for grouped content: a header, a body and an optional footer on a raised surface.
npx prodular-ui add cardDefault
Header with an action, rows split by hairlines, and a footer with a top divider.
import { Badge, BadgeDot } from "@/components/ui/badge"import { Button } from "@/components/ui/button"import { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle,} from "@/components/ui/card"
const rows = [ { label: "Seats", value: "18 of 25 used" }, { label: "Next invoice", value: "$4,320.00 on Mar 14, 2027" }, { label: "Payment method", value: "Visa ending 4242" },]
export default function CardDefault() { return ( <Card className="w-full max-w-md"> <CardHeader> <CardTitle>Team plan</CardTitle> <CardDescription>Billed annually for the Halden workspace.</CardDescription> <CardAction> <Badge variant="success"> <BadgeDot /> Active </Badge> </CardAction> </CardHeader> <CardContent> <dl className="divide-y"> {rows.map((row) => ( <div key={row.label} className="flex items-center justify-between gap-4 py-2.5 first:pt-0 last:pb-0"> <dt className="text-muted-foreground">{row.label}</dt> <dd className="font-medium tabular-nums">{row.value}</dd> </div> ))} </dl> </CardContent> <CardFooter className="justify-end"> <Button variant="outline">Manage seats</Button> <Button>Upgrade plan</Button> </CardFooter> </Card> )}Metrics
KPIs sit in one strip with vertical dividers, not in a grid of smaller cards.
import { ArrowDownRightIcon, ArrowUpRightIcon } from "lucide-react"
import { Badge } from "@/components/ui/badge"import { Button } from "@/components/ui/button"import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle,} from "@/components/ui/card"
const metrics = [ { label: "Monthly recurring revenue", value: "$48,290", delta: "+6.4%", up: true }, { label: "Net revenue retention", value: "112%", delta: "+2.1%", up: true }, { label: "Churned accounts", value: "3", delta: "-2", up: false },]
export default function CardMetrics() { return ( <Card className="w-full max-w-2xl"> <CardHeader> <CardTitle>Revenue</CardTitle> <CardDescription>Last 30 days, compared with the previous period.</CardDescription> <CardAction> <Button variant="ghost" size="sm"> View report </Button> </CardAction> </CardHeader> <CardContent> <div className="grid grid-cols-1 divide-y sm:grid-cols-3 sm:divide-x sm:divide-y-0"> {metrics.map((m) => ( <div key={m.label} className="flex flex-col gap-1.5 py-3 first:pt-0 last:pb-0 sm:px-5 sm:py-0 sm:first:pl-0 sm:last:pr-0"> <span className="text-[13px] text-muted-foreground">{m.label}</span> <div className="flex items-center gap-2"> <span className="text-2xl font-semibold tracking-tight tabular-nums">{m.value}</span> <Badge variant={m.up ? "success" : "info"} size="sm"> {m.up ? <ArrowUpRightIcon data-icon="inline-start" /> : <ArrowDownRightIcon data-icon="inline-start" />} {m.delta} </Badge> </div> </div> ))} </div> </CardContent> </Card> )}Small
size="sm" tightens the spacing for side panels and dense dashboards.
import { GitBranchIcon } from "lucide-react"
import { Badge, BadgeDot } from "@/components/ui/badge"import { Button } from "@/components/ui/button"import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle,} from "@/components/ui/card"
export default function CardSmall() { return ( <Card size="sm" className="w-full max-w-xs"> <CardHeader> <CardTitle>Deploy to production</CardTitle> <CardDescription>Started by Priya Raman 4 minutes ago</CardDescription> </CardHeader> <CardContent className="flex items-center justify-between gap-3"> <span className="flex items-center gap-1.5 text-muted-foreground"> <GitBranchIcon className="size-4" /> <span className="font-mono text-[13px]">main @ 8f3c21a</span> </span> <Badge variant="warning" size="sm"> <BadgeDot pulse /> Building </Badge> </CardContent> <CardFooter> <Button variant="outline" size="sm" className="w-full"> View logs </Button> </CardFooter> </Card> )}Anatomy
<Card> <CardHeader> <CardTitle>Team plan</CardTitle> <CardDescription>Billed annually</CardDescription> <CardAction> <Badge variant="success">Active</Badge> </CardAction> </CardHeader> <CardContent>...</CardContent> <CardFooter> <Button>Upgrade plan</Button> </CardFooter></Card>Guidance
Use it when
- Group content that belongs together and is read or acted on as a unit: a plan, a chart, a settings section.
- Put the single most relevant control for the card in CardAction, top right.
- Use CardFooter for the actions that commit or leave the card. It always gets a top divider.
Avoid
- Cards inside cards, or bordered and tinted boxes inside a card. Separate content with divide-y, border-t and spacing.
- A grid of small cards for KPIs. Use one card with a divided strip.
- Cards for every element on a page. If it has no header and no clear boundary, it probably does not need one.
Rules for AI agents
Published in llms-full.txt, the registry and the MCP server, so coding agents follow them when they build with this component.
- Never nest Card inside Card. Inside a card, split sections with border-t or divide-y on the content.
- Give every card a CardTitle. Add CardDescription only when it says something the title does not.
- Place a Table directly in CardContent. The content drops its side padding and the table aligns its first and last columns with the card.
- Use size="sm" for cards narrower than about 320px.
- Do not override the card padding with p-* classes. The spacing comes from --card-spacing so density themes keep working.
- Use size="xs" for dense surfaces such as map cards and inspector sections instead of overriding --card-spacing.
- Use variant="plain" for a card-shaped section inside a surface that already is one.
API
CardHeader
| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | Add border-b to divide the header from the content. The bottom padding follows. |
CardAction
| Prop | Type | Default | Description |
|---|---|---|---|
| children | ReactNode | One button, badge or menu, aligned to the top right of the header. |
CardFooter
| Prop | Type | Default | Description |
|---|---|---|---|
| children | ReactNode | Actions. Rendered below a hairline divider. |
Card
| Prop | Type | Default | Description |
|---|---|---|---|
| size | "default" | "sm" | "xs" | "default" | Padding and gaps. xs is for inspectors, map cards and dense panels. |
| variant | "default" | "plain" | "default" | plain drops the border, background and shadow. |
Installation
The Prodular UI CLI adds the source to components/ui with everything it depends on. In a project that already uses shadcn/ui, the shadcn CLI installs it from the Prodular registry too. See Installation for the first setup.
npx prodular-ui add card# or, with the registry in components.jsonnpx shadcn@latest add @prodular/card