Designer 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.
- table
A wall of `Card` reads as a feed of independent records, each consumed as a standalone unit (a product, a profile); `Table` presents the same records as scannable rows so users compare attributes across many records. Use Table for inventory / contacts / transactions where the cross-row attribute comparison is the primary task; use Card when each record is consumed on its own and the comparison is incidental. The decision test: does the user scan attributes across rows (Table) or consume each record's hierarchy independently (Card)?
Card is a content-led container with hierarchical title, body, optional media, and action affordances — the workhorse of dashboards, content feeds, and grid layouts. Three structural variants (elevated, outlined, flat) cover the emphasis ladder; orientation toggles between vertical and horizontal layouts; the interactive property turns the whole surface into a single activator via the overlay pattern. The reference documents the slot anatomy, the click-handler contract, the variant-vs-state distinction that recurs in Figma drift, and the cross-framework expression of card-as-link.
Implementations
How specific libraries realise the canonical anatomy. Each entry records the deltas between the canon and the library's surface.
Card import { Card, Flex, Box, Avatar, Text, Inset } from '@radix-ui/themes';
{/* Basic freeform card — no dedicated slots */}<Card size="2" variant="surface"> <Flex gap="3" align="center"> <Avatar radius="full" fallback="T" /> <Box> <Text as="p" weight="bold">Teodros Girmay</Text> <Text as="p" color="gray">Engineering</Text> </Box> </Flex></Card>
{/* Card with flush media via Inset */}<Card size="2" variant="surface"> <Inset clip="padding-box" side="top" pb="current"> <img src="/hero.jpg" alt="Hero image" style={{ display: 'block', width: '100%' }} /> </Inset> <Text as="p" weight="bold">Card title</Text> <Text as="p" color="gray">Supporting body text.</Text></Card>
{/* Interactive card via asChild */}<Card asChild size="2" variant="surface"> <a href="/destination"> <Text as="p" weight="bold">Linked card</Text> </a></Card>Divergence
| From | Type | → To | Rationale |
|---|---|---|---|
anatomy[media] | reshaped | Inset component (flush wrapper) containing a consumer-supplied img or video element; no dedicated card slot. | Radix Themes provides no media slot on Card. Flush-edge imagery is achieved by nesting the Radix `Inset` component (https://www.radix-ui.com/themes/docs/components/card, fetched 2026-05-05) as a child of Card with props `clip="padding-box" side="top" pb="current"`. This delegates all aspect-ratio, lazy-load, and alt-text decisions to the consumer — Radix makes no canonical claim about media handling inside cards. |
anatomy[eyebrow] | omitted | — | Radix Card has no eyebrow slot or prop. The component is an unstyled container; an eyebrow label is composed by the consumer using Radix `Text` with a colour or size prop. No first-class slot exists. (https://www.radix-ui.com/themes/docs/components/card, fetched 2026-05-05) |
anatomy[title] | omitted | — | Radix Card has no title slot. Heading text is placed as a free child using Radix `Text` (e.g. `<Text as="p" weight="bold">`). The library does not enforce heading semantics or an accessible-name relationship between the title and the card surface; these are consumer responsibilities. (https://www.radix-ui.com/themes/docs/components/card, fetched 2026-05-05) |
anatomy[subtitle] | omitted | — | No subtitle slot. Consumer-composed alongside title using Radix `Text` with a muted colour prop. Radix Themes Card is a purely structural container and does not segment content by hierarchy. (https://www.radix-ui.com/themes/docs/components/card, fetched 2026-05-05) |
anatomy[body] | omitted | — | No body slot. Radix Card accepts arbitrary children; prose, structured content, and nested components are placed directly in the card with no wrapping slot providing spacing or semantic context. (https://www.radix-ui.com/themes/docs/components/card, fetched 2026-05-05) |
anatomy[action-group] | omitted | — | Radix Card ships no action slot or footer-action region. Action buttons are composed as free children of Card using Radix `Button` or similar primitives. No spacing contract, keyboard-order guarantee, or primary/secondary action grouping is provided by the library. (https://www.radix-ui.com/themes/docs/components/card, fetched 2026-05-05) |
anatomy[footer] | omitted | — | No footer slot. Metadata or attribution content is placed as free children; the library imposes no visual weight distinction between footer and body content. (https://www.radix-ui.com/themes/docs/components/card, fetched 2026-05-05) |
axes.variants[elevated] | renamed | variant="classic" | The canonical `elevated` variant — a card with a shadow that lifts it above the page surface — maps to Radix `variant="classic"`, which applies a box-shadow. Radix does not use the word "elevated". The `classic` name reflects Radix Themes' design vocabulary, not the cross-library concept. (https://github.com/radix-ui/themes/blob/main/packages/radix-ui-themes/src/components/card.props.tsx, fetched 2026-05-05) |
axes.variants[outlined] | renamed | variant="surface" | The canonical `outlined` variant — a card demarcated by a visible border — maps to Radix `variant="surface"`, which renders a subtle border and background fill. Radix chose "surface" to align with its colour-token naming system rather than the structural term "outlined". (https://github.com/radix-ui/themes/blob/main/packages/radix-ui-themes/src/components/card.props.tsx, fetched 2026-05-05) |
axes.variants[flat] | renamed | variant="ghost" | The canonical `flat` variant — no border and no shadow — maps to Radix `variant="ghost"`, which removes border and background. Radix labels it "ghost" by analogy with ghost buttons; the structural semantics (zero visual boundary) match the canonical `flat` concept. (https://github.com/radix-ui/themes/blob/main/packages/radix-ui-themes/src/components/card.props.tsx, fetched 2026-05-05) |
axes.properties[interactive] | reshaped | asChild prop (polymorphic rendering as <a> or <button>) | The canonical `interactive: boolean` enables whole-card activation via an overlay pattern. Radix achieves the same effect through `asChild`, which renders Card as the passed child element (typically `<a href>` or `<button>`), making the entire card surface the interactive activator. There is no `interactive` prop; the affordance is expressed by substituting the root element rather than toggling a flag. (https://www.radix-ui.com/themes/docs/components/card, fetched 2026-05-05) |
axes.properties[orientation] | omitted | — | Radix Card has no `orientation` prop. Horizontal layout (media on the leading edge) is achieved by the consumer composing Radix `Flex` with `direction="row"` around the card's children. The library provides no canonical layout variant for this. (https://www.radix-ui.com/themes/docs/components/card, fetched 2026-05-05) |
axes.properties[density] | reshaped | size prop ("1" | "2" | "3" | "4" | "5"; default "1") | The canonical `density` axis (comfortable / compact) maps imperfectly to Radix `size`, which is a five-step numeric scale controlling the card's padding. Radix's `size` is finer-grained and does not cleanly map to the two-value density enum; it also controls border-radius alongside padding. The canonical two-step density concept is subsumed into a wider design-token scale. (https://github.com/radix-ui/themes/blob/main/packages/radix-ui-themes/src/components/card.props.tsx, fetched 2026-05-05) |
axes.properties[density] | extended | + `size` accepts Responsive<"1" | "2" | "3" | "4" | "5"> — five steps with responsive breakpoint objects (e.g. `size={{ initial: '1', md: '2' }}`), giving per-breakpoint padding and radius control that the canonical `density` enum does not model. | Radix Themes exposes responsive prop objects on many of its sizing props. This is a first-class Radix-Themes affordance absent from the canonical axis, which only distinguishes comfortable vs compact. (https://www.radix-ui.com/themes/docs/components/card, fetched 2026-05-05) |
axes.states[selected] | omitted | — | Radix Card carries no `data-selected` state or `aria-selected` attribute. Selectable card collections are not a Radix Themes primitive concern — the library ships separate `CheckboxCards` and `RadioCards` components for selection semantics rather than toggling a state on the base Card. The canonical `selected` data state has no equivalent on `Card` itself. (https://www.radix-ui.com/themes/docs/components/card, fetched 2026-05-05) |
axes.states[loading] | omitted | — | Radix Card has no `loading` data state or skeleton affordance. A loading skeleton is consumer-composed (e.g. using Radix Skeleton component inside the card's children). The canonical `loading` data state has no equivalent on the Card primitive itself. (https://www.radix-ui.com/themes/docs/components/card, fetched 2026-05-05) |
Why this audit reads the way it does
Radix Themes Card is a minimal styled-container primitive — a `<div>` with theme tokens applied for padding, border-radius, background, and optional shadow. It ships three named variants (surface / classic / ghost) that map cleanly but with renamed terminology to the canonical elevated / outlined / flat axis, and a five-step numeric size scale that subsumes the canonical two-step density. The Card has no internal anatomy: no media, eyebrow, title, subtitle, body, action, or footer slots. Every structural division the canonical anatomy defines must be composed by the consumer from Radix primitives (Text, Flex, Box, Button, Inset). This is a deliberate design choice — Radix Themes is a composable primitive layer, not a pre-structured content component. The canonical `interactive` boolean is realised through the `asChild` prop (polymorphic root element), which makes the whole card surface a single anchor or button without an explicit flag. This means the overlay-pattern complexity described in the canonical mistakes section is the consumer's responsibility to implement correctly. All anatomy divergences are `omitted` (no library slots match canonical slots). Variant divergences are `renamed` (one-to-one terminology map). Property divergences are `reshaped` (asChild for interactive, size for density) plus one `extended` for the responsive size affordance.
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 |
primary-action
from action-group | instance | Button instance, primary variant, inline-end position |
secondary-action
from action-group | instance | Button instance, secondary variant |
footer | frame | Auto-layout horizontal frame; smaller text style |
Token usage per slot
media- radius
- corner
radius.md
- corner
eyebrow- color
- foreground
color.text.muted
- foreground
- typography
- size
text.xs - weight
weight.medium - tracking
tracking.wide
- size
title- spacing
- blockPadding
spacing.tight
- blockPadding
- color
- foreground
color.text.primary
- foreground
- typography
- size
text.lg - weight
weight.semibold - lineHeight
leading.tight
- size
subtitle- color
- foreground
color.text.muted
- foreground
- typography
- size
text.sm - weight
weight.regular - lineHeight
leading.snug
- size
body- spacing
- blockPadding
spacing.compact
- blockPadding
- color
- foreground
color.text.primary
- foreground
- typography
- size
text.md - lineHeight
leading.normal
- size
primary-action- spacing
- gap
spacing.tight
- gap
footer- spacing
- padding
spacing.compact - gap
spacing.tight
- padding
- color
- foreground
color.text.muted
- foreground
- typography
- size
text.xs
- size
Figma ↔ Code property map
| Figma | Kind | Code | Notes |
|---|---|---|---|
Variant | Enum | variant | Maps elevated / outlined / flat. |
Orientation | Enum | orientation | vertical / horizontal — Figma encodes as a separate Variant property, not bundled with the visual variant. |
Density | Enum | 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 | Slot | 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 | — |
Motion
| Transition | Duration token |
|---|---|
hoverLift | motion.duration.fast |
selectToggle | motion.duration.fast |
Responsive behaviour
| Breakpoint | Change |
|---|---|
breakpoint.sm | At and below, the `horizontal` orientation collapses to vertical regardless of the prop — media stacks above body, the leading-edge layout cannot survive narrow viewports without truncating either the image aspect or the body's measure. Media aspect relaxes from 16:9 to 4:3 for better thumb-scroll density. |
breakpoint.md | Above this width, the `orientation` property is honoured as authored. Density `compact` becomes the default for cards rendered in multi-column grids; `comfortable` for full-width detail cards. |
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.
Variants, properties, states
Variants
Structurally different versions of the component.
elevated outlined flat 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 mismatches
- 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.
Contracts
Non-negotiable contracts
HTML specHTML interactive element semantics + WAI-ARIA Authoring Practices When the whole card is interactive, the activator is a single real `<a href>` (navigation) or `<button>` (in-page action) — never a `<div onclick>` and never `role="button"` on the card root. Nested actions remain reachable via the pseudo-element overlay pattern (the title's anchor extends a `::before` pseudo over the card, nested buttons sit on a higher stacking context).
Wrapping the entire card in `<a>` traps nested buttons — keyboard users cannot activate inner controls, and SR reads the nested buttons as part of the link's accessible name. `<div onclick>` makes the card unfocusable and unannounceable. The overlay pattern is the only construction that keeps both the whole-card-clickable affordance and the nested-actions contract.
Canon Card and Tile and ListItem are not interchangeable. Card is content-led (hierarchical title + body + media + actions); Tile is image-led (visual content primary, text supplementary or absent); ListItem is a horizontal list row with leading icon + primary + secondary + trailing affordances. The decision test is "what is the user scanning for?" — text hierarchy, visual content, or row-of-rows metadata.
Mixing the three contracts produces composite components that read incoherently — text-heavy "tiles" lose the gallery-scan affordance; image-led "cards" force designers to invent media-dominant variants the canon does not endorse; row-style cards collapse the list/grid distinction that drives layout choice.
Canon Interactive states (hover / focus-visible / active / disabled) are NOT modelled as Figma variants. Variants are reserved for structurally different versions (`elevated` / `outlined` / `flat`); states are documented once and rendered via CSS pseudo-classes and `aria-disabled` / `aria-selected`.
Modelling states as variants explodes the matrix (3 variants × 4 interactive states × 2 orientations × 2 densities = 48+ Figma variants) and forces designers and developers to translate the variant matrix into pseudo-classes by hand on every implementation. The anatomy splits intentionally to keep both surfaces small.
Vocabulary drift
- Material 3
Card- Material 3 picks `elevated` and `outlined` as canonical variants matching this canon; `flat` does not appear in Material's roster but ships in Polaris and Atlassian as a low-emphasis variant.
- Polaris
Card- Polaris ships a Card primitive with built-in section slots (`Card.Section`) — finer-grained than the canonical anatomy's body slot. The composite contract matches; the slot granularity differs.
- Carbon
Tile (selectable / clickable)- Carbon labels its content-led card primitive "Tile", producing terminology collision with this canon's image- led `Tile`. Implementation audits document the per-library naming via `componentName` and the contract-vs-naming distinction matters because Carbon-Tile ≈ canonical-Card, not canonical-Tile.
- Atlassian
Card- Atlassian's Card matches the canonical content-led contract; the elevated / outlined / flat axis maps to Atlassian's `appearance` prop.
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.
Accessibility hints
| 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. | |
primary-action | Real button with accessible name. First in DOM order; visual position is inline-end via logical properties so RTL mirroring is automatic. If the whole card is also clickable, see mistake "card-as-link-nested-buttons" for the correct overlay pattern. | |
secondary-action | Real button with accessible name. Second in DOM order; visually inline-start of the primary action in LTR. | |
footer | Footer content is not the card's accessible name. If it carries status, use aria-live on the chip itself, not the card. |