A content container — an optional media region plus a padded content block holding a header, description and footer. layout picks the arrangement: vertical (media above), horizontal (media beside), or cover (media behind the content, under a legibility scrim). Sized xs–xl; data-density scales each size further.
Registry-only
No headless primitive — this component ships only as a copied styled file, so there is no Headless mode. primitiv add card installs it whichever mode you are reading.
Playground
Preview
Winter light
First light over the Cairngorms, shot on a cold clear morning.
import{Card,CardMedia,CardContent,CardHeader,CardTitle,CardDescription,CardFooter,}from"@/components/ui/card";import{Button}from"@/components/ui/button";<Cardlayout="vertical"size="md"elevation="flat"><CardMedia><imgsrc="/photo.jpg"alt=""/></CardMedia><CardContent><CardHeader>{/* asChild picks the heading level for your outline; a bare <CardTitle>…</CardTitle> renders an <h3>. */}<CardTitleasChild><h4>Winter light</h4></CardTitle></CardHeader><CardDescription>First light over the Cairngorms.</CardDescription><CardFooter><Buttonsize="sm"variant="secondary">Share</Button><Buttonsize="sm">View</Button></CardFooter></CardContent></Card>
Density is set by a data-density ancestor — the Context system, not a Card prop.
Installation
npx primitiv add card
pnpm dlx primitiv add card
yarn dlx primitiv add card
bunx primitiv add card
Import
import{Card}from"@/components/ui/card";
No headless primitive — this one ships only as a copied styled surface, so primitiv add is the only way in, whichever mode you are reading.
Anatomy
Seven parts, composed. Card is the frame (and owns layout / size / elevation / scrim); CardMedia is the optional image region; CardContent is the padded block that holds everything else — and it owns all the padding, so CardHeader, CardDescription and CardFooter add none of their own. CardHeader is a flex row (the title stretches, leading/trailing slots hug); CardTitle is an <h3> (asChild to fit the page's outline); CardFooter is the action row (justify aligns it). Media and content are always siblings — for layout="cover" the CSS positions the media behind the content, no re-nesting.
<Card><CardMedia>{/* img / picture / video */}</CardMedia><CardContent><CardHeader><CardTitle>Title</CardTitle></CardHeader><CardDescription>Body copy.</CardDescription><CardFooter>{/* actions */}</CardFooter></CardContent></Card>
Props
Generated from the copied file’s props type and contract.json. Never hand-maintained.
Card
Extends HTMLDivElement — every native attribute of that element is accepted and forwarded.
Prop
Type
Default
Description
asChild
boolean
—
Render the single child element instead of a wrapping <div>, merging
the card classes onto it — e.g. <Card asChild><a href="...">...</a></Card>
for a card that is itself one link.
coverForegroundDark
"white" | "black"
"white"
The title/description colour while the app is in dark theme. Only
has an effect when layout="cover". See coverForegroundLight.
coverForegroundLight
"white" | "black"
"white"
The title/description colour while the app is in light theme.
Only has an effect when layout="cover". Limited to the two absolute
(non-theme-flipping) tones — the scrim itself is always
color/absolute-black regardless of theme, so "white" is legible
against it in every case except an unusually bright photo. Set this
independently of coverForegroundDark: a given photo's legibility
doesn't necessarily track the app's own light/dark switch.
layout
"vertical" | "horizontal" | "cover"
vertical
How the media region relates to the content.
size
"xs" | "sm" | "md" | "lg" | "xl"
md
Card size; data-density scales each size further.
elevation
"flat" | "raised"
flat
Resting shadow depth.
scrim
"soft" | "medium" | "strong"
medium
Strength of the legibility gradient. Only rendered by layout="cover".
CardMedia
Extends HTMLDivElement — every native attribute of that element is accepted and forwarded.
Prop
Type
Default
Description
children
ReactNode
—
The image, <picture>, <video> or <svg> to show. It is sized to
cover the region, so it needs no sizing of its own.
inset
boolean
false
Inset the media from the card edge and round its corners, instead of
bleeding it flush to the border.
Flush media is deliberately square-cornered — the card's own
overflow: hidden supplies the outer radius, so giving the media its
own would also round the inner seam where it meets the content and
leave a visible notch.
CardContent
Extends HTMLDivElement — every native attribute of that element is accepted and forwarded.
No props of its own.
CardHeader
Extends HTMLDivElement — every native attribute of that element is accepted and forwarded.
No props of its own.
CardTitle
Extends HTMLHeadingElement — every native attribute of that element is accepted and forwarded.
Prop
Type
Default
Description
asChild
boolean
—
Render the single child element instead of a wrapping <h3>, merging
the title classes onto it — use this to set the correct heading level
for the surrounding document outline.
CardDescription
Extends HTMLParagraphElement — every native attribute of that element is accepted and forwarded.
Prop
Type
Default
Description
asChild
boolean
—
Render the single child element instead of a wrapping <p>.
CardFooter
Extends HTMLDivElement — every native attribute of that element is accepted and forwarded.
Prop
Type
Default
Description
justify
"start" | "center" | "end"
"end"
Horizontal alignment of the footer's actions. Button alignment is a
real per-composition decision, so it is a prop rather than a fixed
house rule.
Styling contract
28 CSS custom properties on .primitiv-card — mode-agnostic. These names are the stable surface; the values are not.
A card is a container, not a role. By itself it is a <div> with no semantics — the meaning comes from what you put in it. CardTitle renders a real <h3>, so it lands in the document outline; use asChild on it to pick the level that fits (an <h2> in a section of cards, say) rather than leaving heading levels to jump.
Decide what the media announces. A decorative photo takes alt="" (as every example here does) so a screen reader skips it; a meaningful image gets a real alt. CardMedia does not force either — it is your <img>.
One card, one link — via asChild on the root. Making the whole card a link is far better than an <a> wrapped around a grid of text and buttons. But a card that is itself a link must contain no other interactive elements: a <button> or second <a> inside a link is invalid and unusable by keyboard. Keep the footer actions for non-link cards.
Footer actions are real controls, so order them by importance.CardFooter's justify is visual only; the DOM order is the tab order, so put the primary action first in the markup even if it sits on the right visually.
layout and size are presentational and change nothing in the accessibility tree — a cover card reads the same as a vertical one. The scrim and coverForeground* settings are about visual legibility, which is itself an accessibility concern: check the title's contrast against the actual photo, since the scrim only helps so much over a busy image.
elevation is decoration. A raised card is not more important to assistive technology than a flat one; if a card is genuinely more significant, say so in its content, don't rely on the shadow.
Examples
Every example below reacts to the density control. Currently showing Styled mode.
A complete card (the anatomy in practice)
The full composition: an optional CardMedia, then a CardContent holding a CardHeader (with the CardTitle), a CardDescription, and a CardFooter of actions. CardContent supplies the padding and the vertical rhythm between those regions — deliberately, so there is one padding box and no doubled seam where regions meet. CardTitle renders an <h3>; use asChild on it to set the heading level that fits your page outline.
Winter light
First light over the Cairngorms, shot on a cold clear morning.
import{Card,CardMedia,CardContent,CardHeader,CardTitle,CardDescription,CardFooter,}from"@/components/ui/card";import{Button}from"@/components/ui/button";<Card><CardMedia><imgsrc="/photo.jpg"alt=""/></CardMedia><CardContent><CardHeader>{/* asChild picks the heading level for your outline; a bare <CardTitle>…</CardTitle> renders an <h3>. */}<CardTitleasChild><h4>Winter light</h4></CardTitle></CardHeader><CardDescription>First light over the Cairngorms.</CardDescription><CardFooter><Buttonsize="sm"variant="secondary">Share</Button><Buttonsize="sm">View</Button></CardFooter></CardContent></Card>
Layouts
layout decides where the media sits relative to the content: "vertical" (the default) stacks media on top, "horizontal" puts it to the side, and "cover" places it behind the content with a legibility scrim. Media and content are the same two siblings in every layout — only the CSS changes — so you switch presentation with one prop, not a different tree.
Winter light
First light over the Cairngorms, shot on a cold clear morning.
Winter light
First light over the Cairngorms, shot on a cold clear morning.
Winter light
First light over the Cairngorms, shot on a cold clear morning.
CardMedia's inset prop chooses how the image meets the frame. Flush (the default) bleeds the media to the border with square corners — the card's own overflow: hidden supplies the outer radius, and giving the media its own would round the inner seam into a visible notch. inset pulls the media in from the edge and rounds all four of its corners, for the more contained, panel-in-a-panel look.
Winter light
First light over the Cairngorms, shot on a cold clear morning.
Winter light
First light over the Cairngorms, shot on a cold clear morning.
import{Card,CardMedia,CardContent,CardHeader,CardTitle,CardDescription,CardFooter,}from"@/components/ui/card";import{Button}from"@/components/ui/button";{/* flush (default) */}<Card><CardMedia><imgsrc="/photo.jpg"alt=""/></CardMedia><CardContent><CardHeader>{/* asChild picks the heading level for your outline; a bare <CardTitle>…</CardTitle> renders an <h3>. */}<CardTitleasChild><h4>Winter light</h4></CardTitle></CardHeader><CardDescription>First light over the Cairngorms.</CardDescription><CardFooter><Buttonsize="sm"variant="secondary">Share</Button><Buttonsize="sm">View</Button></CardFooter></CardContent></Card>{/* inset + rounded */}<Card><CardMediainset><imgsrc="/photo.jpg"alt=""/></CardMedia><CardContent><CardHeader>{/* asChild picks the heading level for your outline; a bare <CardTitle>…</CardTitle> renders an <h3>. */}<CardTitleasChild><h4>Winter light</h4></CardTitle></CardHeader><CardDescription>First light over the Cairngorms.</CardDescription><CardFooter><Buttonsize="sm"variant="secondary">Share</Button><Buttonsize="sm">View</Button></CardFooter></CardContent></Card>
Cover layout and the scrim
Under layout="cover" the content sits over the media, and a scrim gradient keeps it legible against any photo — "soft", "medium" (default) or "strong". The scrim is a pseudo-element on the card, so it costs nothing in the other layouts. The text is coverForegroundLight / coverForegroundDark (both default "white", the only two absolute non-theme-flipping tones) — set them per photo, since a bright image can need dark text regardless of the app's own light/dark mode.
Soft
Scrim strength soft.
Medium
Scrim strength medium.
Strong
Scrim strength strong.
import{Card,CardMedia,CardContent,CardHeader,CardTitle,CardDescription,CardFooter,}from"@/components/ui/card";import{Button}from"@/components/ui/button";<Cardlayout="cover"scrim="strong"coverForegroundLight="white"><CardMedia><img src="/photo.jpg" alt="" /></CardMedia><CardContent><CardHeader><CardTitleasChild><h4>Winter light</h4></CardTitle></CardHeader><CardDescription>First light over the Cairngorms.</CardDescription></CardContent></Card>
The whole card as a link (asChild)
asChild on the root merges the card onto your own element, so the entire card can be a single link. Prefer this to putting an <a> around a <div> full of other controls: one card, one link, one focus stop. Do not then also put buttons or links inside it — nested interactive elements inside a link are invalid; a card that is itself a link should have no other controls (drop the footer actions).
import{Card,CardMedia,CardContent,CardHeader,CardTitle,CardDescription,CardFooter,}from"@/components/ui/card";import{Button}from"@/components/ui/button";<CardasChildlayout="horizontal"><ahref="/photos/winter-light"><CardMediainset><img src="/photo.jpg" alt="" /></CardMedia><CardContent><CardHeader><CardTitleasChild><h4>Winter light</h4></CardTitle></CardHeader><CardDescription>First light over the Cairngorms.</CardDescription></CardContent></a></Card>