Installation
npx shadcn@latest add @pixeldosa/cardThe example above also uses Button. To copy it as-is, install everything it needs:
npx shadcn@latest add @pixeldosa/card @pixeldosa/buttonRequires `cn` at `@/lib/utils`, which `npx shadcn init` creates. CardTitle and CardDescription render `div` elements, not heading tags, matching the wider shadcn/ui convention — supply your own heading element as content if the card needs to participate in the page's document outline. In `orientation="horizontal"`, Card does not auto-arrange its children into an image column and a content column — wrap the header/content/footer parts in your own flex column alongside CardImage.
Usage
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
CardFooter,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
export function OrderSummary() {
return (
<Card>
<CardHeader>
<CardTitle>Plated dosa set</CardTitle>
<CardDescription>Serves two, ready in 12 minutes.</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Crisp rice-and-lentil crepe, coconut chutney, and sambar on the side.
</p>
</CardContent>
<CardFooter className="border-t">
<Button className="w-full">Add to order</Button>
</CardFooter>
</Card>
);
}
CardAction places a trailing element — a menu trigger, a badge — in the header's
second column, aligned to the top-right regardless of how much title or description
text wraps:
<CardHeader>
<CardTitle>Plated dosa set</CardTitle>
<CardDescription>Serves two, ready in 12 minutes.</CardDescription>
<CardAction>
<Button variant="ghost" size="icon" aria-label="More options">
<MoreIcon />
</Button>
</CardAction>
</CardHeader>
CardImage adds a full-bleed image region. In the default vertical orientation it
sits first and renders as a top banner; give Card orientation="horizontal" and it
becomes a fixed-width, left-rounded side panel instead — wrap the remaining parts in
your own flex column so they sit beside it:
<Card orientation="horizontal" className="max-w-lg">
<CardImage src="/cabin.jpg" alt="A wooden cabin overlooking a mountain valley" />
<div className="flex flex-1 flex-col justify-between">
<CardHeader>
<CardTitle>Ridgeline cabin</CardTitle>
<CardDescription>Alpine views, sleeps four.</CardDescription>
</CardHeader>
<CardFooter>
<Button size="sm">Reserve now</Button>
</CardFooter>
</div>
</Card>
A pricing card needs none of the above — it's the same Card / CardHeader /
CardTitle / CardDescription / CardContent / CardFooter parts from the first
example, just with a feature list and a price as content. Highlight a tier with a
plain className override (border-2 border-primary) rather than a prop, since
"highlighted" is a one-off treatment, not a structural variant:
<Card className="border-2 border-primary">
<CardHeader>
<CardTitle>Pro</CardTitle>
<CardDescription>For agencies</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<span className="text-3xl font-semibold">$40</span>
<ul className="flex flex-col gap-2 text-sm text-muted-foreground">
<li>Unlimited downloads</li>
<li>Fully editable files</li>
</ul>
</CardContent>
<CardFooter>
<Button className="w-full">Subscribe</Button>
</CardFooter>
</Card>
Features
- Eight composable parts rather than a props-driven mega-component, so a card can be a summary, a listing, or a pricing tier without any of them being a special case.
- A self-arranging header:
CardHeadergrows a second column automatically when aCardActionis present, keeping the trailing control top-aligned however much the title and description wrap. - Two orientations:
verticalrendersCardImageas a top banner,horizontalas a fixed-width, left-rounded side panel. Theshowcasevariant removes the default vertical padding and gap for preview-led compositions. - No invented interaction semantics — every part is a plain
div, so making a card actionable stays an explicit decision (see Accessibility).
Variants
The default vertical composition — header, content, and a separated footer:
Crisp rice-and-lentil crepe, coconut chutney, and sambar on the side.
Showcase composition
Use variant="showcase" when a preview or image is the card's primary content. It
removes the default vertical padding and gap so the media can sit flush with the
card edge; compose the remaining content explicitly:
<Card variant="showcase">
<img src="/preview.png" alt="Dashboard preview" className="aspect-video w-full object-cover" />
<CardFooter className="justify-between gap-3 pt-4">
<div>
<CardTitle>Analytics workspace</CardTitle>
<CardDescription>Explore product metrics and trends.</CardDescription>
</div>
<Button variant="outline" size="sm">Open preview</Button>
</CardFooter>
</Card>
With a header action
CardAction takes the header's second column, so the trigger stays pinned top-right
no matter how long the title runs.
Delivered to 1,284 subscribers.
Highlighted
A highlighted tier is a className override, not a prop — "highlighted" is a one-off
treatment, not a structural variant worth encoding in the API.
For the horizontal orientation and the full listing/pricing compositions, see the examples at the top of this page.
Props
Every part forwards className and all native props of the element it renders.
| Part | Renders | Notes |
|---|---|---|
Card | div | The bordered container. bg-card / text-card-foreground, 1px border-border, rounded-xl. Takes orientation="vertical" | "horizontal" (default vertical) and variant="default" | "showcase" (default default). showcase removes vertical padding and gaps for preview-led layouts. |
CardHeader | div | Grid layout; adds a second column automatically when a CardAction is present. |
CardTitle | div | Not a heading element — nest a real heading tag as its child if the card is part of the page outline. |
CardDescription | div | Muted, small text. |
CardAction | div | Self-aligns to the header's top-right. |
CardContent | div | Horizontal padding only; pairs with Card's vertical py-6. |
CardFooter | div | Add the border-t class yourself when the footer should be visually separated. |
CardImage | img | Full-bleed banner (vertical) or side panel (horizontal) — see above. Requires alt. |
Accessibility
- All parts are plain
divs — Card has no interaction semantics of its own, so there is nothing for Radix to manage here. CardTitledeliberately does not render a heading element, matching the wider shadcn/ui convention. If the card's title should appear in the page's heading outline, render a heading element as its child rather than relying onCardTitleitself for that.- Interactive cards (a whole card acting as a link or button) are not this
component's job — wrap the actionable content in a
Button asChildor an anchor instead of addingonClicktoCarddirectly, so keyboard and screen-reader semantics stay correct. CardImagehas no defaultalt— supply one that describes the image's content. If the image is purely decorative (rare for a listing-style card), passalt=""explicitly rather than omitting the prop.
why it's built this way
Engineering Notes
Exposed as eight composable parts rather than a single component with header/footer/title/image props, so a Block composing Card can rearrange or omit regions without fighting a monolithic API. CVA expresses two purposeful axes: orientation controls CardImage's banner-versus-side-panel geometry, while the showcase variant removes default vertical padding and gaps for preview-led catalogue cards. A pricing card still needs no special Card capability — it composes the existing parts and its own content. Depth uses border-border rather than a shadow, matching the system's dark-mode-first 'borders over shadows' decision documented in the design tokens.
how it moves, and when it doesn't
Motion Notes
None. Card is compositional infrastructure with no interaction state of its own — motion belongs on components built on top of it (a future SpotlightCard or an interactive card treatment), not baked into the base primitive, per the pillar rule that motion must earn its place.