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
| Variant | Use |
|---|---|
| elevated | Shadow/lift on a neutral background |
| outlined | Border only, no shadow — dense lists |
| filled | Light background (surface.subtle) |
| ghost | Almost 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, meaningfulalt(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
- Build
Card+ slots with no business logic. - Create 3 stories: static, with media, clickable (link in title).
- Check keyboard use and no nested interactives.