Loading…
Loading…
A container for grouping related content and actions about a single subject.
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:
When NOT to use a Card:
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.
| 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 | 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 | 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.
| 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 |
| 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 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 |
Cards are the ideal canvas for advanced CSS effects:
See our Design Tokens Complete Guide for token naming conventions.
| 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) |
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.
| 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). |
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>
| 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.
<article> for standalone content. A blog post preview, product listing, or user profile is standalone content — semantically, it's an article.align-items: stretch on the grid container, and push the footer to the bottom with margin-top: auto inside the card.alt text. If the image is decorative, use alt="".<!-- 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>| 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) |
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.