Overview
The Card is a container component that groups related content and actions about a single subject. It's a self-contained unit of information — a product listing, a user profile summary, a dashboard metric, a blog post preview. Cards turn flat page layouts into scannable, modular interfaces.
Cards work because they leverage the Gestalt principle of common region: content inside a shared boundary is perceived as a group. The border, shadow, or background color creates that boundary, and users instinctively treat everything inside it as related.
The most important design decision with cards is interactivity. A card can be entirely static (a content container), partially interactive (with action buttons inside), or fully interactive (the entire card is clickable). Each approach has different accessibility implications and user expectations.
When to use a Card:
- To display a collection of related items in a grid or list (products, articles, team members)
- To group content with its associated actions (a post with like, share, comment buttons)
- To create dashboard widgets (stats, charts, activity feeds)
- As a container for a form section or settings group
When NOT to use a Card:
- For a single block of continuous content — just use a section with a heading
- For tabular, comparable data — use a Table instead
- For a list of simple items with 1–2 attributes — use a List
- As a layout primitive — cards are content containers, not generic wrappers. Use CSS Grid or a Stack for layout.
Elevate your card design with our Shadow Generator, experiment with glassmorphism effects using the Glassmorphism Generator, create compelling backgrounds with the Gradient Generator, and fine-tune corner rounding with the Border Radius Generator. Use the Spacing Calculator to establish consistent internal padding.
Variants
Card Variants
| Variant | Description | Best For |
|---|---|---|
| Elevated | White/surface background with box-shadow. Lifts off the page. | Default variant. Works on any background. |
| Outlined | Border instead of shadow. Flat, no elevation. | Dense layouts where shadows create visual noise. |
| Filled | Colored or tinted background, no border or shadow. | Highlighting specific cards (featured, promoted). |
| Ghost | No border, no shadow, no distinct background. Content-only. | When cards are implied by grid layout and explicit containers add clutter. |
| Glassmorphic | Semi-transparent background with backdrop-filter: blur(). | Modern, layered designs. Create with our Glassmorphism Generator. |
| Interactive | Entire card is clickable. Hover state with shadow increase or slight lift. | Product listings, article previews, link cards. |
Layout Variants
| Layout | Description |
|---|---|
| Vertical | Media on top, content below. Most common card layout. |
| Horizontal | Media on the left, content on the right (or reversed). Good for compact listings. |
| Media-only | Full-bleed image with overlay text. Image galleries, hero cards. |
| Header-body-footer | Structured sections: header (title + avatar), body (content), footer (actions). |
| Stat card | Large metric value, label, and optional trend indicator. Dashboard widgets. |
Size Variants
| Size | Min Width | Padding | Use Case |
|---|---|---|---|
| Compact | 200px | 12px | Data-dense dashboards, mobile |
| Standard | 280px | 16–20px | Default grid cards |
| Wide | 100% | 24px | Full-width feature cards, blog posts |
Use our Spacing Calculator to define consistent padding across card sizes.
Properties
Card Properties
| Property | Type | Default | Description |
|---|---|---|---|
variant | 'elevated' | 'outlined' | 'filled' | 'ghost' | 'elevated' | Visual style |
padding | 'none' | 'sm' | 'md' | 'lg' | 'md' | Internal padding |
as | 'div' | 'article' | 'a' | 'button' | 'div' | Semantic element. Use article for standalone content, a for link cards. |
href | string | — | Makes the entire card a link (renders as <a>) |
onClick | () => void | — | Makes the entire card clickable |
interactive | boolean | false | Enables hover/active states (shadow lift, background shift) |
fullWidth | boolean | false | Stretches to fill container width |
children | ReactNode | — | Card content |
Card Sub-components
| Component | Purpose |
|---|---|
Card.Header | Title, subtitle, avatar, and optional action (menu, close button) |
Card.Media | Image or video, typically full-bleed (no padding) |
Card.Body | Main content area with default padding |
Card.Footer | Actions (buttons) and metadata (timestamp, author) |
Token Mappings
Design Token Mappings
| Token Category | Token Example | Card Usage |
|---|---|---|
| Color – Surface | --color-surface, --color-surface-elevated | Card background. Elevated cards use a slightly lighter surface. |
| Color – Border | --color-border-subtle | Outlined variant border |
| Color – Hover | --color-surface-hover | Interactive card hover background |
| Shadow | --shadow-sm (rest), --shadow-md (hover) | Elevated variant. Generate values with Shadow Generator. |
| Border Radius | --radius-lg (12px) or --radius-xl (16px) | Card corners. More rounding = friendlier feel. Experiment with Border Radius Generator. |
| Spacing | --space-4 (16px), --space-6 (24px) | Internal padding. Use Spacing Calculator for consistent values. |
| Typography | --font-size-lg, --font-weight-semibold | Card title styling |
| Transition | --duration-fast (150ms) | Hover shadow/transform transition |
Advanced Visual Effects
Cards are the ideal canvas for advanced CSS effects:
- Gradient backgrounds: Create unique card backgrounds with our Gradient Generator
- Glassmorphism: Semi-transparent, blurred backgrounds with our Glassmorphism Generator
- Layered shadows: Multiple shadow layers create realistic depth — use the Shadow Generator
See our Design Tokens Complete Guide for token naming conventions.
States
Card States
| State | Visual Change | Behavior |
|---|---|---|
| Default | Base styling per variant (shadow, border, or background) | Static content container |
| Hover (interactive only) | Shadow increases (shadow-sm → shadow-md), optional translateY(-2px) lift | Cursor changes to pointer. Indicates the card is clickable. |
| Active / Pressed (interactive only) | Shadow decreases or card scales down slightly (scale(0.98)) | Confirms the click registered |
| Focus (interactive only) | Visible focus ring (2px, high-contrast) around the card border | Keyboard navigation. Must meet WCAG 2.1 SC 2.4.7. |
| Loading / Skeleton | Card shape with animated skeleton placeholders for content | Use Skeleton components matching the card's content layout |
| Disabled | Reduced opacity, no hover effect | Rare for cards, but applicable for dashboard widgets that aren't available |
| Selected | Primary-tinted border or background, checkmark badge | In multi-select card grids (e.g., selecting photos) |
Hover Animation Best Practices
For interactive cards, a subtle lift on hover creates a "picking up" metaphor:
.card-interactive {
transition: transform 150ms ease, box-shadow 150ms ease;
}
.card-interactive:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.card-interactive:active {
transform: translateY(0);
box-shadow: var(--shadow-sm);
}
Don't overdo the lift — 2–4px maximum. Larger values feel cartoon-ish. Configure these values with our Shadow Generator.
Accessibility
WCAG Requirements
| Criterion | Level | Requirement |
|---|---|---|
| SC 1.3.1 Info and Relationships | A | Use semantic HTML: <article> for standalone content cards, <section> for grouped sections. Cards with a heading should use proper heading levels. |
| SC 2.5.8 Target Size (Minimum) | AA | Interactive cards must have at least 24×24px click targets. Full card links satisfy this easily. |
| SC 2.4.7 Focus Visible | AA | Interactive cards must show a visible focus indicator. |
| SC 1.4.3 Contrast (Minimum) | AA | Card text must meet 4.5:1 contrast against the card background. Use Contrast Checker. |
| SC 1.4.11 Non-text Contrast | AA | Card borders must meet 3:1 contrast against the page background (for outlined variant). |
The "Clickable Card" Problem
Making an entire card clickable is one of the trickiest accessibility challenges. Here are the approaches, ranked:
1. Nested link with ::after pseudo-element (recommended):
<article class="card" style="position: relative;">
<h3><a href="/product/123" class="card-link">Product Name</a></h3>
<p>Description text...</p>
<button type="button" class="card-action" style="position: relative; z-index: 1;">
Add to cart
</button>
</article>
<style>
.card-link::after {
content: "";
position: absolute;
inset: 0;
}
</style>
This approach makes the entire card clickable via the link's pseudo-element, while allowing other interactive elements (buttons) to remain individually clickable by stacking above via z-index.
2. Wrapping in <a> (problematic):
<!-- ❌ Avoid: nesting interactive elements inside <a> is invalid HTML -->
<a href="/product/123">
<article class="card">
<button>Add to cart</button> <!-- Invalid: button inside anchor -->
</article>
</a>
3. JavaScript click handler on the card:
<!-- ⚠️ Acceptable, but requires role and keyboard handling -->
<article class="card" role="link" tabindex="0" onclick="navigate('/product/123')" onkeydown="handleEnter(event)">
...
</article>
Keyboard Interaction
| Key | Action |
|---|---|
| Tab | Moves focus to the card (if interactive) or to the first focusable element inside |
| Enter | Activates the card link |
| Space | Activates the card link (if using role="link") |
Always test your card contrast with the Contrast Checker — especially when using gradient backgrounds, glassmorphism effects, or images with text overlays.
Usage Guidelines
Do's
- ✅ Use
<article>for standalone content. A blog post preview, product listing, or user profile is standalone content — semantically, it's an article. - ✅ Include a heading in each card. Cards without headings are hard to scan and harder for screen readers to navigate. Use the appropriate heading level in context.
- ✅ Keep card heights consistent in a grid. Use
align-items: stretchon the grid container, and push the footer to the bottom withmargin-top: autoinside the card. - ✅ Use image aspect ratios consistently. All cards in a grid should use the same image ratio (16:9, 4:3, 1:1). Inconsistent ratios create a ragged, unprofessional grid.
- ✅ Limit actions to 1–2 per card. A card with 5 buttons isn't a card — it's a dashboard widget that needs its own page.
Don'ts
- ❌ Don't overload cards with content. A card should be a summary, not a detail page. If you need a paragraph of text, 4 badges, 3 buttons, and a chart — use a dedicated page or an expanded view.
- ❌ Don't make the entire card clickable AND have buttons inside without handling the stacking correctly (see accessibility section). Users will accidentally trigger the wrong action.
- ❌ Don't use cards for sequential content. A list of steps is not a card grid. Use a Stepper or ordered list.
- ❌ Don't nest cards inside cards. It creates visual confusion and accessibility headaches.
- ❌ Don't use cards just for visual decoration. If the card border/shadow doesn't serve a grouping purpose, drop it and use simpler layout.
Content Guidelines
- Title: 2–8 words. Descriptive, not clickbait-y. Should make sense out of context.
- Description: 1–3 sentences maximum. The card is a preview — save the details for the detail page.
- Image: Always include
alttext. If the image is decorative, usealt="". - Actions: 1 primary action ("View", "Buy now"), 1 optional secondary ("Save", "Share"). Use icon buttons for compact card footers.
Code Snippets
<!-- Basic content card -->
<article class="card card-elevated">
<img
class="card-media"
src="/images/project-thumbnail.jpg"
alt="Mountain landscape at sunset"
loading="lazy"
/>
<div class="card-body">
<h3 class="card-title">Mountain Explorer</h3>
<p class="card-description">
A curated collection of mountain photography from the Swiss Alps.
</p>
</div>
<footer class="card-footer">
<span class="card-meta">12 photos</span>
<div class="card-actions">
<button type="button" class="btn btn-ghost btn-icon" aria-label="Save to favorites">
♡
</button>
<button type="button" class="btn btn-primary btn-sm">View</button>
</div>
</footer>
</article>
<!-- Interactive link card (entire card clickable) -->
<article class="card card-elevated card-interactive" style="position: relative;">
<img
class="card-media"
src="/images/article-thumb.jpg"
alt=""
loading="lazy"
/>
<div class="card-body">
<h3>
<a href="/blog/design-tokens" class="card-link">
Design Tokens: A Complete Guide
</a>
</h3>
<p class="card-description">
Learn how design tokens bridge the gap between design and development.
</p>
<time class="card-meta" datetime="2026-02-15">Feb 15, 2026</time>
</div>
</article>
<!-- Stat card for dashboards -->
<div class="card card-outlined" role="group" aria-label="Total revenue">
<div class="card-body">
<p class="card-label">Total Revenue</p>
<p class="card-stat">$48,352</p>
<p class="card-trend card-trend-up">
<span aria-hidden="true">↑</span> 12.5% from last month
</p>
</div>
</div>import { forwardRef, type ReactNode, type HTMLAttributes } from "react";
type Variant = "elevated" | "outlined" | "filled" | "ghost";
type Padding = "none" | "sm" | "md" | "lg";
interface CardProps extends HTMLAttributes<HTMLElement> {
variant?: Variant;
padding?: Padding;
interactive?: boolean;
as?: "div" | "article" | "section";
}
const Card = forwardRef<HTMLElement, CardProps>(
({ variant = "elevated", padding = "md", interactive = false, as: Tag = "div", className = "", children, ...props }, ref) => {
const classes = [
"card",
`card-${variant}`,
`card-pad-${padding}`,
interactive && "card-interactive",
className,
].filter(Boolean).join(" ");
return (
<Tag ref={ref as any} className={classes} {...props}>
{children}
</Tag>
);
},
);
Card.displayName = "Card";
function CardMedia({ src, alt, aspectRatio = "16/9" }: { src: string; alt: string; aspectRatio?: string }) {
return <img className="card-media" src={src} alt={alt} loading="lazy" style={{ aspectRatio }} />;
}
function CardBody({ children }: { children: ReactNode }) {
return <div className="card-body">{children}</div>;
}
function CardFooter({ children }: { children: ReactNode }) {
return <footer className="card-footer">{children}</footer>;
}
export { Card, CardMedia, CardBody, CardFooter };
// Usage
<Card as="article" variant="elevated" interactive>
<CardMedia src="/images/project.jpg" alt="Project thumbnail" />
<CardBody>
<h3><a href="/project/123" className="card-link">Design System Kit</a></h3>
<p>A complete design system starter with tokens and components.</p>
</CardBody>
<CardFooter>
<span className="text-muted">Updated 2 days ago</span>
<button type="button" className="btn btn-primary btn-sm">Open</button>
</CardFooter>
</Card>
// Stat card
<Card variant="outlined" padding="lg">
<CardBody>
<p className="card-label">Active Users</p>
<p className="card-stat">12,847</p>
<p className="card-trend up">↑ 8.3% this week</p>
</CardBody>
</Card>Design Systems
Cross-System Comparison
| Feature | Material 3 | Shadcn/ui | Radix | Ant Design |
|---|---|---|---|---|
| Component | Card (Elevated, Filled, Outlined) | Card (styled <div>) | No card primitive | Card |
| Variants | Elevated, Filled, Outlined | No built-in variants (Tailwind classes) | N/A | default, bordered, inner, type="inner" |
| Sub-components | CardHeader, CardContent, CardActions | CardHeader, CardTitle, CardDescription, CardContent, CardFooter | N/A | Card.Meta, Card.Grid |
| Clickable card | Custom implementation | Custom | N/A | hoverable prop |
| Loading state | Not built-in | Not built-in | N/A | loading prop with skeleton |
| Cover image | CardMedia | Custom | N/A | cover prop |
| Actions | CardActions container | Custom footer | N/A | actions prop (renders in footer) |
Notable Approaches
Material 3 formalizes three card variants with distinct use cases: Elevated (default, shadow-based), Filled (tinted background, no shadow — ideal for surfaces that already have elevation), and Outlined (bordered, flat — for when shadows would create visual noise in dense layouts). This three-variant system covers virtually every card need.
Shadcn/ui provides the most granular sub-component composition: CardHeader, CardTitle, CardDescription, CardContent, and CardFooter. Each is a simple styled <div> with Tailwind classes. This composability is flexible but means you're assembling cards from 5+ components every time.
Ant Design ships a loading prop that automatically renders a skeleton placeholder inside the card — a pragmatic feature that saves developers from building skeleton states manually. Ant also supports Card.Grid for creating gridded layouts inside a single card.
Radix has no card primitive — and shouldn't. A card is primarily a visual container, not a behavioral primitive. There's no complex interaction logic to abstract. Build your card with semantic HTML (<article>) and CSS.
Interesting trend (2025–2026): "Bento grid" layouts — cards of varying sizes arranged in an asymmetric grid — have become ubiquitous in landing pages and dashboards. CSS Grid's grid-template-areas and span make this achievable without JavaScript. Pair with our Spacing Calculator for consistent gaps.