Dev view

Card

A bounded container that groups a coherent unit of content — typically a title, supporting body, optional media, and optional actions — and lifts it as a single perceivable object on the page.

When to use

Use

When grouping a coherent unit of content — title plus body plus optional media plus optional actions — that should perceive as a single object on the page. Typical hosts: dashboards, marketing grids, search results, content collections.

Avoid

For dense list rows where every row is structurally identical and visual separation is purely a horizontal rule — that is `ListItem`. For decorative content tiles in a grid where each tile is primarily an image with a label — `Tile`. For announcing transient information — `Banner` or `Toast`.

Versus related

  • tile

    `Tile` is image-led with minimal supporting text; `Card` is content-led with hierarchical title plus body plus media. A wall of Tiles reads as a gallery; a wall of Cards reads as a feed.

  • list-item

    `ListItem` lives inside an explicit `<ul>` or `<ol>` and cooperates with sibling rows for keyboard navigation and selection semantics. `Card` is a standalone object with no implicit sibling relationship.

Highlight
Fig 1.1 · Card · Dev view
Dev

Code anatomy

Slot Code slot Semantic
media media img-or-video
eyebrow eyebrow text-with-role-or-tag
title title heading-or-link
subtitle subtitle text
body body prose-or-children
actions actions button-group
footer footer contentinfo-region
Both

Variants, properties, states

Variants

Structurally different versions of the component.

elevatedoutlinedflat

Properties

The same component, parameterised.

PropertyType
interactiveboolean
orientationvertical | horizontal
densitycomfortable | compact

States

Browser/user-driven (interactive) vs. app-driven (data).

KindStates
interactive
hoverfocus-visibleactivedisabled
data
selectedloading
Dev

Cross-framework expression

FrameworkStructure mechanismVariant mechanism
Web Components named slots (<slot name="media">, <slot name="title">, …) attributes reflected to properties (variant="elevated", orientation="horizontal")
React compound components (Card.Media, Card.Title, Card.Body, …) or explicit slot props props with class-variance-authority / tailwind-variants for the variant/property axis
Angular (signals) ng-content with select="[card-media]" / [card-title] etc., and signal-based input() for slots input<'elevated' | 'outlined' | 'flat'>() and input<'vertical' | 'horizontal'>()
Vue named slots (<slot name="media" />, <slot name="title" />, …) defineProps with literal-union types
Both

Accessibility

Slot Accessibility hint
media Provide alt text for informative images; alt="" for purely decorative media. Do not duplicate the title in alt text.
eyebrow Avoid heading semantics — the eyebrow is metadata, not a heading. If grouped with the title, treat as part of the same labelling relationship via aria-labelledby.
title Use a heading element of the appropriate level for the surrounding document outline. If the card is interactive as a whole, the title also carries the accessible name of the activator.
subtitle If the subtitle qualifies the title's meaning, associate it via aria-describedby on the interactive element.
body Body content keeps its native semantics (lists stay lists, links stay links). Avoid wrapping body in role="text" — it strips semantics from assistive tech.
actions Keep nested buttons keyboard-reachable. If the whole card is also clickable, see mistake "card-as-link-nested-buttons" for the correct overlay pattern.
footer Footer content is not the card's accessible name. If it carries status, use aria-live on the chip itself, not the card.
Both

Accessibility acceptance

Keyboard walk

KeysExpected
TabFor non-interactive cards, focus skips the card surface and lands on nested interactive elements (buttons, links inside actions or the body) in document order. For `interactive: true` cards using the overlay pattern, the title's `<a>` is the single tab stop for the card; nested actions remain individually reachable on subsequent Tab presses (kept on a higher stacking context).
Enter (on overlay link)Activates the card-as-link target. Spacebar does not activate — anchors are Enter-only by native convention.

Screen-reader announcements

TriggerExpected
Focus enters card-as-linkThe title's accessible name followed by "link" (e.g. "Q3 revenue report, link"). The eyebrow, subtitle, and body are not part of the link's accessible name unless explicitly tied via `aria-labelledby`.
`selected` data state setSelection is announced via `aria-selected="true"` on the card's primary actionable element. SR reads "selected" before the accessible name (varies slightly by SR).

axe-core rules to assert

  • link-name
  • color-contrast
  • heading-order
  • image-alt
Dev

Common mistakes

#clickable-card-no-keyboard

Click handler on the card without a focusable activator

Problem

Attaching a click handler to a <div> card with no <a> or <button> inside makes the card unreachable by keyboard and unannounceable to assistive tech.

Fix

Always anchor the activation on a real interactive element (<a href> for navigation, <button> for in-page actions). If the visual treatment requires the whole card to look clickable, use the overlay pattern — never role="button" on the card div.

#variant-explosion-from-states

Modelling interactive states as Figma variants

Problem

Adding hover / focus / active / disabled as variants in Figma causes the variant matrix to explode and forces the developer to hand-translate Figma variants into CSS pseudo-classes.

Fix

Document interactive states once in a separate "states" sheet or component documentation. Reserve Figma variants for structurally different versions (elevated / outlined / flat). Document the same separation in the canonical reference and the production library.

#media-alt-duplicates-title

Image alt text repeats the card title

Problem

Setting `alt` to the title text causes screen readers to announce the title twice when the card is interactive — once as the link name and once as the image alt.

Fix

For decorative / supporting media use `alt=""`. For media that carries information not in the title, write alt that adds the missing information ("Chart showing 12% YoY growth"), not a restatement of the headline.

Figma↔Code mismatches
  1. 01
    Figma

    Variants for hover / focus / active / disabled

    Code

    CSS pseudo-classes (:hover, :focus-visible, :active) and aria/disabled attributes

    Consequence

    Variant explosion in Figma (3 variants × 4 states × 2 orientations = 24+ component variants), and the developer cannot map a Figma "hover variant" to a CSS pseudo-class without manual translation.

    Correct

    Treat interactive states as a separate spec (a "states" sheet or component property documentation) — not as Figma variants. Variants are reserved for structurally different versions (elevated / outlined / flat).

  2. 02
    Figma

    Media-on-top vs. media-on-leading-edge expressed as separate variants

    Code

    A single component with an `orientation` prop ('vertical' | 'horizontal')

    Consequence

    Designers and developers count card variants differently. Designers see 6+ variants; developers see 3 variants × 2 orientations. Implementation drift: designer adds a third orientation in Figma but the prop type is a binary union.

    Correct

    Model orientation as a *property*, not a variant. Document the property in Figma using a component property of type 'variant' that *only* captures orientation, separate from visual variants.

  3. 03
    Figma

    A clickable card built by stacking a "card" component on top of an invisible "button" component

    Code

    An <a>/<button> wrapping the entire card (or a pseudo-element overlay pattern)

    Consequence

    The Figma artifact does not encode the affordance — designers may not realise the whole card must be a single accessible activator, and developers may forget the keyboard story when implementing.

    Correct

    When the card is interactive as a whole, model the activator as an explicit boolean property (`interactive`) on the canonical component. Render it as a single anchor or button with the rest of the card visually inside, using the overlay pattern to keep nested controls reachable.

  4. 04
    Figma

    Selected state expressed via an "outline + filled background" variant

    Code

    A `data-selected` or `aria-selected` attribute toggled by the application

    Consequence

    The selection style is duplicated in two places (variant + CSS) and inevitably drifts. Worse, treating selection as a variant blocks multi-state selection (e.g., selected + disabled).

    Correct

    Document selection as a *data state*, not a variant. The visual treatment is described once and is composable with the interactive states.