Skip to content

Card

A container for grouping related content and actions about a single subject.

  • Tile
  • Content Card

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

VariantDescriptionBest For
ElevatedWhite/surface background with box-shadow. Lifts off the page.Default variant. Works on any background.
OutlinedBorder instead of shadow. Flat, no elevation.Dense layouts where shadows create visual noise.
FilledColored or tinted background, no border or shadow.Highlighting specific cards (featured, promoted).
GhostNo border, no shadow, no distinct background. Content-only.When cards are implied by grid layout and explicit containers add clutter.
GlassmorphicSemi-transparent background with backdrop-filter: blur().Modern, layered designs. Create with our Glassmorphism Generator.
InteractiveEntire card is clickable. Hover state with shadow increase or slight lift.Product listings, article previews, link cards.

Layout Variants

LayoutDescription
VerticalMedia on top, content below. Most common card layout.
HorizontalMedia on the left, content on the right (or reversed). Good for compact listings.
Media-onlyFull-bleed image with overlay text. Image galleries, hero cards.
Header-body-footerStructured sections: header (title + avatar), body (content), footer (actions).
Stat cardLarge metric value, label, and optional trend indicator. Dashboard widgets.

Size Variants

SizeMin WidthPaddingUse Case
Compact200px12pxData-dense dashboards, mobile
Standard280px16–20pxDefault grid cards
Wide100%24pxFull-width feature cards, blog posts

Use our Spacing Calculator to define consistent padding across card sizes.

Properties

Card Properties

PropertyTypeDefaultDescription
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.
hrefstring—Makes the entire card a link (renders as <a>)
onClick() => void—Makes the entire card clickable
interactivebooleanfalseEnables hover/active states (shadow lift, background shift)
fullWidthbooleanfalseStretches to fill container width
childrenReactNode—Card content

Card Sub-components

ComponentPurpose
Card.HeaderTitle, subtitle, avatar, and optional action (menu, close button)
Card.MediaImage or video, typically full-bleed (no padding)
Card.BodyMain content area with default padding
Card.FooterActions (buttons) and metadata (timestamp, author)

Token Mappings

Design Token Mappings

Token CategoryToken ExampleCard Usage
Color – Surface--color-surface, --color-surface-elevatedCard background. Elevated cards use a slightly lighter surface.
Color – Border--color-border-subtleOutlined variant border
Color – Hover--color-surface-hoverInteractive 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-semiboldCard title styling
Transition--duration-fast (150ms)Hover shadow/transform transition

Advanced Visual Effects

Cards are the ideal canvas for advanced CSS effects:

See our Design Tokens Complete Guide for token naming conventions.

States

Card States

StateVisual ChangeBehavior
DefaultBase styling per variant (shadow, border, or background)Static content container
Hover (interactive only)Shadow increases (shadow-sm → shadow-md), optional translateY(-2px) liftCursor 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 borderKeyboard navigation. Must meet WCAG 2.1 SC 2.4.7.
Loading / SkeletonCard shape with animated skeleton placeholders for contentUse Skeleton components matching the card's content layout
DisabledReduced opacity, no hover effectRare for cards, but applicable for dashboard widgets that aren't available
SelectedPrimary-tinted border or background, checkmark badgeIn 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

CriterionLevelRequirement
SC 1.3.1 Info and RelationshipsAUse 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)AAInteractive cards must have at least 24×24px click targets. Full card links satisfy this easily.
SC 2.4.7 Focus VisibleAAInteractive cards must show a visible focus indicator.
SC 1.4.3 Contrast (Minimum)AACard text must meet 4.5:1 contrast against the card background. Use Contrast Checker.
SC 1.4.11 Non-text ContrastAACard 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

KeyAction
TabMoves focus to the card (if interactive) or to the first focusable element inside
EnterActivates the card link
SpaceActivates 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: stretch on the grid container, and push the footer to the bottom with margin-top: auto inside 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 alt text. If the image is decorative, use alt="".
  • Actions: 1 primary action ("View", "Buy now"), 1 optional secondary ("Save", "Share"). Use icon buttons for compact card footers.

Code Snippets

html
<!-- 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>
tsx
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

FeatureMaterial 3Shadcn/uiRadixAnt Design
ComponentCard (Elevated, Filled, Outlined)Card (styled <div>)No card primitiveCard
VariantsElevated, Filled, OutlinedNo built-in variants (Tailwind classes)N/Adefault, bordered, inner, type="inner"
Sub-componentsCardHeader, CardContent, CardActionsCardHeader, CardTitle, CardDescription, CardContent, CardFooterN/ACard.Meta, Card.Grid
Clickable cardCustom implementationCustomN/Ahoverable prop
Loading stateNot built-inNot built-inN/Aloading prop with skeleton
Cover imageCardMediaCustomN/Acover prop
ActionsCardActions containerCustom footerN/Aactions 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.

Data DisplayTileContent Card