Bridge 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.
Figma↔Code mismatches
Where designer and developer worlds typically misalign on this component.
- 01 Figma
Variants for hover / focus / active / disabled
CodeCSS pseudo-classes (:hover, :focus-visible, :active) and aria/disabled attributes
ConsequenceVariant 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.
CorrectTreat 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).
- 02 Figma
Media-on-top vs. media-on-leading-edge expressed as separate variants
CodeA single component with an `orientation` prop ('vertical' | 'horizontal')
ConsequenceDesigners 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.
CorrectModel 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.
- 03 Figma
A clickable card built by stacking a "card" component on top of an invisible "button" component
CodeAn <a>/<button> wrapping the entire card (or a pseudo-element overlay pattern)
ConsequenceThe 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.
CorrectWhen 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.
- 04 Figma
Selected state expressed via an "outline + filled background" variant
CodeA `data-selected` or `aria-selected` attribute toggled by the application
ConsequenceThe 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).
CorrectDocument selection as a *data state*, not a variant. The visual treatment is described once and is composable with the interactive states.
Variants, properties, states
Variants
Structurally different versions of the component.
elevatedoutlinedflat Properties
The same component, parameterised.
| Property | Type |
|---|---|
interactive | boolean |
orientation | vertical | horizontal |
density | comfortable | compact |
States
Browser/user-driven (interactive) vs. app-driven (data).
| Kind | States |
|---|---|
interactive | hoverfocus-visibleactivedisabled |
data | selectedloading |
Figma ↔ Code property map
| Figma | Type | Code | Notes |
|---|---|---|---|
Variant | Variant | variant | Maps elevated / outlined / flat. |
Orientation | Variant | orientation | vertical / horizontal — Figma encodes as a separate Variant property, not bundled with the visual variant. |
Density | Variant | density | — |
Interactive | Boolean | interactive | Toggles the card-as-link overlay pattern. In Figma it controls hover state visibility; in code it conditionally wraps the card surface with the interactive activator. |
Has Media | Boolean | media | Slot-visibility toggle; code conditionally renders the media slot. |
Media | Instance Swap | media | Swaps the media component instance (image, video, iframe). |
Has Eyebrow | Boolean | eyebrow | — |
Eyebrow | Text | eyebrow | — |
Title | Text | title | — |
Has Subtitle | Boolean | subtitle | — |
Subtitle | Text | subtitle | — |
Body | Text | body | For non-prose bodies, Figma uses an Instance Swap on a 'Body Slot' instead. |
Has Actions | Boolean | actions | — |
Has Footer | Boolean | footer | — |
Figma anatomy
| Slot | Figma type | Hint |
|---|---|---|
media | frame | Aspect-ratio-locked frame; image fill or instance swap |
eyebrow | text | Text style "Eyebrow" / "Overline"; uppercase or small-caps |
title | text | Heading text style; bound to a component property for content |
subtitle | text | Subtitle text style; lighter weight than title |
body | text-or-frame | Auto-layout text frame, or nested component instance for richer payloads |
actions | frame | Auto-layout horizontal frame containing button instances |
footer | frame | Auto-layout horizontal frame; smaller text style |
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 |
Cross-framework expression
| Framework | Structure mechanism | Variant 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 |
Internationalisation
RTL · mirroring
Horizontal-orientation cards flip their leading-edge media to the right side via logical `inline-start` properties. Eyebrow / title / subtitle / body inherit document direction; numerals and dates should follow the locale's preferred numeral system. Action button order reverses: the primary action moves from inline-end (right in LTR) to inline-end (left in RTL) — the *logical* position is unchanged, the visual position mirrors. Decorative media that carries directionality (left-pointing arrow icons, before/after photo pairs) needs explicit alt-text or composition handling.
Text expansion
Title is clamped to 2 lines (`-webkit-line-clamp: 2`) by canonical convention; subtitle to 3; body free-flow. Eyebrow may wrap to a second line under heavy expansion (DE/RU). Action labels follow Button's expansion rules. Multi-column card grids may need to re-flow at narrower widths for languages with longer-than-average text.
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. |
Accessibility acceptance
Keyboard walk
| Keys | Expected |
|---|---|
Tab | For 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
| Trigger | Expected |
|---|---|
| Focus enters card-as-link | The 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 set | Selection 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-namecolor-contrastheading-orderimage-alt
Common mistakes
#card-as-link-nested-buttons
Card-as-link with nested action buttons
Wrapping the whole card in an <a> turns the entire surface into a single tab stop and traps nested buttons — keyboard users cannot activate the inner controls, and screen readers announce the nested buttons as part of the link's name.
Use the pseudo-element overlay pattern: keep the card as a regular container, give the title a real <a> with `::before` covering the whole card via absolute positioning, and let nested actions sit on a higher stacking context (z-index) so clicks on them aren't intercepted. The link receives the card's accessible name from the title.
#clickable-card-no-keyboard
Click handler on the card without a focusable activator
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.
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
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.
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
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.
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.