Card — spec¶
Status: example artifact for
component-spec-writerskill Generated by:skills/component-spec-writer, citing Ant DesignCard, MUICard, shadcn-uicard
Purpose¶
A card groups related content into a single contained surface. Cards are the workhorse of dashboards, lists, settings, and landing pages. Easy to over-use — every group does NOT need to be a card.
When to use a card vs alternatives¶
| Pattern | Use |
|---|---|
| Card (with border or background) | Group is genuinely a unit of content the user might select, navigate to, or compare. |
| Section (no border, just heading + whitespace) | Logical grouping where boundary doesn't matter. Default for forms. |
| List item | Many comparable items in a feed. Cards in a vertical list = visual noise. |
| Tile (square, image-led) | Grid of equal items where image dominates. |
Rule of thumb: if you remove the card border and the grouping is still clear from spacing + typography, don't use a card.
Anatomy¶
Card (root surface)
├── [Card.Cover]? full-bleed image/media at top
├── Card.Header
│ ├── [Card.HeaderActions]? (top-right slot — menu, close)
│ ├── Card.Title
│ └── [Card.Description]?
├── Card.Body (main content)
└── [Card.Footer]? (actions, metadata)
| Part | Required | Notes |
|---|---|---|
| Card | yes | the container |
| Cover | optional | full-bleed image; sits above header |
| Header | typically | unless body is self-titling (e.g., a metric card with the metric as the only content) |
| Title | yes if Header present | semantic <h3> or <h4> based on page hierarchy |
| Description | optional | secondary line under title |
| HeaderActions | optional | top-right; menu button, close, status badge |
| Body | yes | primary content |
| Footer | optional | actions, timestamps, metadata |
API (composition-style)¶
<Card>
<Card.Cover src="..." alt="..." />
<Card.Header>
<Card.Title>Project Aurora</Card.Title>
<Card.Description>Design system for the new dashboard</Card.Description>
<Card.HeaderActions>
<IconButton aria-label="More options" />
</Card.HeaderActions>
</Card.Header>
<Card.Body>
Body content...
</Card.Body>
<Card.Footer>
<Button variant="outline">View</Button>
</Card.Footer>
</Card>
| Prop (root) | Type | Default | Description |
|---|---|---|---|
variant |
"outline" \| "filled" \| "elevated" |
"outline" |
Visual treatment. |
interactive |
boolean |
false |
Hover/focus states; entire card clickable. |
as |
ElementType |
"div" |
Render-as. Use "a" or "button" when interactive. |
href |
string |
— | If set, root renders as <a> for navigation. |
onClick |
(e: MouseEvent) => void |
— | Sets interactive=true. |
padding |
"none" \| "sm" \| "md" \| "lg" |
"md" |
Internal padding. none for image-led cards. |
Variants¶
variant¶
| Value | Background | Border | Shadow | Use |
|---|---|---|---|---|
outline (default) |
--color-bg-default |
1px --color-border-default |
none | Default — clean, neutral. Most cards. |
filled |
--color-bg-elevated |
none | none | Subtle backdrop without a border line. |
elevated |
--color-bg-default |
none | --shadow-2 |
Lifted feel — landing pages, marketing. Avoid in dense UIs. |
Choose one variant per surface and stick to it. Mixing outline and elevated cards on the same page reads as inconsistent.
Interactive cards¶
interactive={true} (or providing onClick/href) adds:
- Cursor pointer
- Hover: bg shift to --color-bg-subtle (outline/filled) or shadow strengthens (elevated)
- Focus-visible: 2px ring (same as Button)
- The full card is clickable, not just the title.
Required: render as <a> (with href) or <button> — not <div onClick>. Cite knowledge/a11y/keyboard-and-focus.md.
If the card has both a primary navigation AND inner buttons (e.g., card click navigates, footer has "Edit" and "Delete"), use the "block link" pattern:
- The whole card is wrapped in <a>.
- Inner buttons stop propagation and have higher z-index.
- The card title's text is the inner <a> for screen reader, with the rest visually clickable via ::before pseudo.
Padding rules¶
padding |
Header | Body | Footer | Use |
|---|---|---|---|---|
sm |
12px | 12px | 12px | Dense lists, list-as-cards |
md (default) |
16px | 16px | 16px | Standard product cards |
lg |
24px | 24px | 24px | Marketing, hero cards |
none |
0 | 0 | 0 | Image-led cards where Cover takes full width |
For image-led cards: padding="none", manually pad the text sections. The cover should bleed to the card's border-radius edge.
Tokens consumed¶
--color-bg-default
--color-bg-elevated
--color-bg-subtle (interactive hover)
--color-border-default
--color-text-primary
--color-text-secondary
--space-md
--space-lg
--radius-lg (cards usually larger than buttons)
--shadow-2 (elevated variant)
--transition-fast (interactive hover)
Accessibility¶
- Default:
<div>is fine for non-interactive cards. Title inside should be a heading (<h3>typically). - Interactive: render as
<a>(navigation) or<button>(action). Keyboard reachable via Tab. Activates with Enter (always) and Space (button only). - Block link pattern when card has primary nav + inner controls: only one focus stop for the card's primary purpose; inner buttons get their own focus stops. Don't nest interactive elements.
- Heading hierarchy: card titles should not all be
<h2>. Match the page's hierarchy. A card on a<h1>page might use<h3>for its title — never skip a level. - Image alt text:
Coverimage needsaltdescribing meaning, not "card image". If decorative,alt="". - Empty state: a card with no content should not render an empty box. Render a placeholder with a clear CTA, or omit the card entirely.
- Focus indicator: 2px ring on the card (interactive only), 2px offset from border. Same
--color-focus-ringas Button.
Code examples¶
// Static info card — content card
<Card>
<Card.Header>
<Card.Title>Total revenue</Card.Title>
<Card.Description>Last 30 days</Card.Description>
</Card.Header>
<Card.Body>
<p className="text-3xl font-semibold">₩12,847,500</p>
<p className="text-sm text-text-secondary">+12% vs prior period</p>
</Card.Body>
</Card>
// Interactive card — navigates on click
<Card interactive href="/projects/aurora">
<Card.Cover src="/aurora-thumb.jpg" alt="Aurora project preview" />
<Card.Header>
<Card.Title>Project Aurora</Card.Title>
<Card.Description>Design system rebuild</Card.Description>
</Card.Header>
<Card.Body>
<p>12 components shipped this week.</p>
</Card.Body>
</Card>
// Card with menu + actions
<Card>
<Card.Header>
<Card.Title>Notifications</Card.Title>
<Card.HeaderActions>
<IconButton aria-label="Settings"><SettingsIcon /></IconButton>
</Card.HeaderActions>
</Card.Header>
<Card.Body>
<NotificationList items={notifs} />
</Card.Body>
<Card.Footer>
<Button variant="link">See all</Button>
</Card.Footer>
</Card>
// Block-link pattern
<Card interactive className="relative">
<Card.Header>
<Card.Title>
<a href="/post/123" className="absolute inset-0 z-0" aria-label="Read post: How we shipped Aurora" />
How we shipped Aurora
</Card.Title>
</Card.Header>
<Card.Body>
<p>Last week we wrapped a 3-month effort to...</p>
</Card.Body>
<Card.Footer className="relative z-10">
<Button onClick={(e) => { e.stopPropagation(); like(); }}>Like</Button>
<Button onClick={(e) => { e.stopPropagation(); share(); }}>Share</Button>
</Card.Footer>
</Card>
Edge cases¶
- Card grid that wraps awkwardly at certain widths: use
repeat(auto-fit, minmax(280px, 1fr))to set a sensible min before wrapping. - Card with image of unknown aspect ratio: use
aspect-ratioCSS to enforce a ratio for the cover; lazy-load withloading="lazy". - Cards in dark mode look flat:
outlinevariant works as-is.elevatedshadow becomes ineffective on dark bg — increase shadow opacity OR switch to a +2% lighter background instead of shadow. - Long text in card body: don't truncate by default. Truncate only in space-constrained layouts (compact list cards) and provide a "see more" path.
- Long titles: wrap to 2 lines, then truncate with
…. Providetitleattribute with full text. - Skeleton loading: render the same shape (header + body + footer) with skeleton placeholders rather than a generic block. Cite knowledge/patterns/ux-guidelines.md.
Don't¶
- Don't put cards inside cards. The visual nesting reads chaotic. Use sections within a card instead.
- Don't make every grouping a card. Use whitespace + headings — they're free.
- Don't mix card variants on the same page (e.g., some
elevated, someoutline). - Don't put a primary CTA in a card without context — a card is a container, not a top-level surface.
- Don't make a card interactive AND put another interactive surface inside without using the block-link pattern. Nested clickable is broken.
References¶
- Ant Design:
refs/ant-design/components/card/—Card,Card.Meta,Card.Grid. Strong support for tabbed cards (tabListprop) and grid cards. Most exhaustive. - MUI:
refs/mui/packages/mui-material/src/Card/— composition:Card,CardHeader,CardMedia,CardContent,CardActions,CardActionArea(the wrapper that makes a card clickable). MUI'sCardActionAreais the clearest implementation of the interactive-card pattern. - shadcn-ui:
refs/shadcn-ui/apps/v4/registry/new-york-v4/ui/card.tsx— composition withCard,CardHeader,CardTitle,CardDescription,CardContent,CardFooter,CardAction. Cleanest minimal version.
API choices made:
- Composition over prop overload: matches all three references' direction. Single <Card title=... description=... actions=...> API was rejected — slot-based scales.
- Card.HeaderActions as a slot inside Card.Header rather than a separate top-level child: keeps the action's positional semantics tied to the header, making CSS simpler.
- variant over MUI's elevation number: 3 named variants are easier to choose than 24 shadow elevation steps. (MUI's elevation={1..24} is over-flexible.)
- interactive prop OR onClick/href infers it: ergonomic — common case requires no extra prop, advanced case (interactive without click handler — rare) is still expressible.
Cross-reference¶
- knowledge/components/INDEX.md
- knowledge/a11y/keyboard-and-focus.md — block-link pattern
- knowledge/layout/spacing-and-grid.md — card-grid sizing