Card — a frame for meaning, not “yet another box”

A card groups related content: title, media, description, actions. In a design system, Card is a layout primitive with tokens, not a dump of props like hasShadow && isHoverable && isBlog && isPricing.

When a Card is appropriate

  • A list of entities of one type (courses, products, people).
  • A summary with short copy and a CTA.
  • A settings / widget panel on a dashboard.

When you do not need a Card: a single paragraph, an entire page, a decorative border with no semantic grouping. Extra cards create visual noise.

Anatomy (slots)

Prefer composition over one monolith:

<Card>
  <Card.Media>
    <img src="/course.png" alt="" />
  </Card.Media>
  <Card.Body>
    <Card.Title as="h3">Design System</Card.Title>
    <Card.Description>Tokens, components, documentation</Card.Description>
  </Card.Body>
  <Card.Footer>
    <Button size="sm">Open</Button>
  </Card.Footer>
</Card>

Slots: Media, Header/Title, Body, Footer/Actions. Consumers compose what they need; the DS does not try to guess every layout in the world.

Surface variants

VariantUse
elevatedShadow/lift on a neutral background
outlinedBorder only, no shadow — dense lists
filledLight background (surface.subtle)
ghostAlmost no chrome; border on hover

Padding sizes (sm | md | lg) should map to spacing tokens; radius to radius.md / radius.lg.

Interactive cards

If the entire card is clickable:

  • Prefer one clear focus target: a link/button in the title, or <a class="card"> with a clear name.
  • Do not nest buttons inside a large <a> — that is invalid HTML and an a11y nightmare.
  • “Card-as-link” pattern: the title is the link, stretched hit area via CSS (::after), secondary actions are separate controls outside the link.

Hover/focus styles should show affordance: slight lift, border change, focus ring on the focusable element.

Responsive

  • On mobile — one column, media on top.
  • Grid: grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)).
  • Images: fixed aspect ratio (aspect-ratio), object-fit: cover, meaningful alt (or empty if purely decorative and a title is nearby).

Base styles

.card {
  background: var(--color-surface);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  overflow: hidden;
}
.card__body {
  padding: var(--space-4);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

Practice

  1. Build Card + slots with no business logic.
  2. Create 3 stories: static, with media, clickable (link in title).
  3. Check keyboard use and no nested interactives.