Docs
Card

Card

Flexible content container with named areas for media, title, description, free-form content and actions. Built on top of Flex, it inherits all the layout factory props.

When title or description are strings, Card automatically wraps them with the appropriate typography; when they are ReactNode, they are rendered as-is — useful for titles with icons or avatars.

When to use

✅ Use when…🚫 Avoid when…
  • To present an entity with its own visual identity (product, plan, post, resource).
  • When the content combines media, text and a primary action (e.g. a product card with photo, description and an "Add" button).
  • In lists/grids where each item needs a consistent border and padding.
  • For running text without an action or visual emphasis. Use a Flex with a subtle background.
  • As a modal/dialog. Use the Modal component.
  • When you need shadows and depth. Combine with Paper or use Paper directly.

Card with media

The media slot is full-bleed: the image meets the Card's top and side edges (inheriting the corner radius), while the p padding stays on the text slots below it. Pass any ReactNode (typically an <Image cover />).

A title here

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus vel metus sit amet diam placerat finibus in non lacus.

<Card
  width={300}
  title="A title here"
  actionComponent={<Button fullWidth text="Accept" variant="contained" />}
  media={<Image cover src={cover} />}
>
  <Flex flexDirection="row" wrap="wrap" alignItems="center" mb="small" gap={4}>
    <Icon icon={emojiStar} color="warning" />
    <Icon icon={emojiStar} color="warning" />
    <Icon icon={emojiStar} color="warning" />
  </Flex>
  <Text fontSize="xs">Free-form description as children.</Text>
</Card>

Card without media

Just omit the media prop. The title can be a composed ReactNode.

Title without image

The content is just a link:

Open Figma!
<Card
  width={300}
  title={
    <Flex flexDirection="row" wrap="wrap" alignItems="center" gap="small">
      <Icon icon={roundedCheck} />
      <Text variant="h5" text="Title without image" />
    </Flex>
  }
>
  <Text color="primary" variant="p" text="The content is just a link:" />
  <Link href="https://www.figma.com/">Open Figma!</Link>
</Card>

Actions

Pass any ReactNode in actionComponent — a single button, a pair of buttons, links, etc.

Action buttons in the footer.

<Card
  width={300}
  media={<Image cover src={cover} />}
  actionComponent={
    <Flex flexDirection="row" wrap="wrap" alignItems="center" justifyContent="between">
      <Button size="small" color="danger" text="Delete" variant="outlined" />
      <Button size="small" text="Confirm" variant="contained" />
    </Flex>
  }
>
  <Text variant="p">Action buttons</Text>
</Card>

Card is a Grid (Paper surface + named slots)

Card is the Paper-like surface that absorbs Grid. Its slots are named grid areas on the Card itself — media, title, content (description + subtitle + children), action — with a default template that follows the slots present: auto 1fr auto when there is an action, so the footer pins to the foot of an equal-height tile. p and gap pad the Card directly (default large, no inner shell). The page Grid places Cards; a slot that needs grouping uses Flex, never a nested Grid.

Short

One line

Longer

Two lines of copy that make the content row grow while the action stays at the foot.

<Grid medias={{ columns: '1fr 1fr', gap: 'medium' }} height={280}>
  <Card height="100%" title="Short" description="One line" actionComponent={<Link href="#" text="Go" />} />
  <Card height="100%" interactive title="Longer" description="…" actionComponent={<Link href="#" text="Go" />} />
</Grid>

Pass medias to replace the template (keep the area names). interactive announces hover — primary border, level-three glow, 2px lift — for tiles that navigate; the glow is not clipped (overflow becomes visible).

Navigational tiles and responsive slots

An interactive tile that navigates renders as an anchor — set as="a" and href; both are typed on the component's own props (CardProps), so no local augmentation is needed:

<Card as="a" href="/markets/btc" interactive title="BTC" description="$64,120" />

The Card carries the full factory surface, so its own slot layout can react to the Card's width (cq) or the viewport (viewportMedias) — the same Card works in a 280px sidebar and a 900px feed with zero viewport overrides:

<Card
  interactive
  title="BTC"
  description="$64,120"
  cq={{ md: { p: 'large' } }} // reacts to the Card's own width
  viewportMedias={{ xl: { p: 'small' } }} // reacts to the viewport
/>

loading

Properties

Prop
Type
Default
Description
actionComponent
ReactNode
Action component (button, button pair, link). Rendered in the `action` area — the last row of the template, so it pins to the Card's foot when the Card is taller than its content.
alignContent
"start" | "end" | "center" | "between" | "around" | "evenly" | "stretch"
Packing of wrapped lines / grid tracks on the block axis.
as
ElementType<any, keyof IntrinsicElements>
Render the container as a different element or component (`styled-components` `as`).
children
ReactNode
Main content of the card. Appears below the description.
color
string
Theme token (`theme.colors`) used as `color`. Has lower precedence than `contrast`: when both are provided, the contrast calculation wins. Stays `string` (not the typed union): `color` is also an HTML attribute (`<li color>`, etc.) and interfaces extending native props require identical types on the name collision.
containment
string
Containment marker for the `cq` channel: emits `container-type` (+ `container-name` when the string form carries one, e.g. `'inline-size card'`). Apply on the immediate wrapper of the adapting component — NEVER on page-level shells.
cq
Partial<Record<ContainerSizeTypes, ContainerQueryable>>
Container-relative styles per `theme.containerSizes` key.
description
ReactNode
Description/subtitle of the card. Same contract as `title`: a string becomes `BaseText`, any other `ReactNode` is rendered as-is.
download
string | boolean
Anchor `download` hint.
enableBreakpoint
"xs" | "xl" | "sm" | "md" | "lg"
Forces a prop set to apply as if the given breakpoint were active, regardless of the actual viewport size. Useful for stories/storyshots or controlled previews.
forwardedAs
ElementType<any, keyof IntrinsicElements>
Forward the rendered element down a `styled(...)` chain (`styled-components` `forwardedAs`).
href
string
Destination when the tile renders as an anchor (`as="a"`).
ignoreResponsive
boolean
When `true`, fully disables the responsive factory (useful in tests or in components that want to show a single fixed snapshot).
interactive
boolean
false
Announce hover: primary border, `level-three` glow and a 2px lift over the `short` transition (`overflow` becomes `visible` so the glow is not clipped). For tiles that navigate or open something.
justifyItems
"start" | "end" | "center" | "stretch"
Grid containers: inline alignment of every item in its cell.
legibility
"on-photo"
Reading-shadow preset for text over a photographic background (`on-photo`). Replaces the inline `style={{ textShadow }}` in the consumer (brasil_2030 radar, gap A3). Token emission is deferred until a 2nd consumer asks for the raw var (see backlog).
media
ReactNode
Media displayed at the top of the card, rendered **full-bleed**: it meets the Card's top and side edges (and inherits the corner radius), while the `p` padding applies only to the text slots below it. Typically an `<Image>` or a video, but any `ReactNode` is accepted.
medias
GridBreakpoints
Responsive grid configuration. Accepts either a "flat" config (applied as the default) or an object with per-breakpoint keys (`xs`, `sm`, `md`,...). Treat `xs` as mobile-first. @example ```tsx <Grid medias={{ xs: { columns: '1fr', areas: '"a"' }, md: { columns: '1fr 2fr', areas: '"a b"' } }} /> ```
motion
"none" | "fade" | "scale" | "slide-up" | "slide-down" | "slide-left" | "slide-right" | "collapse" | "sheet" | "sheet-top" | "drawer-left" | "drawer-right"
Semantic motion preset. Enter animates on mount via `@starting-style`; exit expects the `data-state="closing"` attribute (see `useExitTransition`). Durations/easings resolve from the `--apollion-motion-*` vars with the theme literal as fallback, so `prefers-reduced-motion` zeroes them centrally. @example ```tsx <Flex motion="slide-up">Toast body</Flex> ```
pageShell
boolean
Centers the element and caps the width at `theme.layout.pageMaxWidth`. The page shell of a classic centered layout.
placeItems
"start" | "end" | "center" | "stretch"
Grid containers: `place-items` shorthand (block + inline).
readable
number | boolean
Makes the `color` legible against the page surface: `true` = WCAG AA (4.5), a number sets a custom floor. Ignored with `contrast`. See the Layout Props concept page for the full semantics.
rel
string
Anchor `rel` (e.g. `noopener noreferrer` for `target="_blank"`).
subtitle
ReactNode
Additional slot rendered in the `content` area between `description` and `children`.
target
HTMLAttributeAnchorTarget
Anchor target (`_blank`, `_self`, …).
title
ReactNode
Main title of the card. When a string, it is rendered in `BaseText` with heading typography; when a `ReactNode`, it is used as-is — useful for including icons, avatars, or custom hierarchy.
transform
string
CSS `transform` value (e.g. `translate(-50%, -33px)`). Never leaks to the DOM as an attribute.
viewportMedias
Partial<Record<keyof BreakPoint<any>, ContainerQueryable>>
Viewport-driven styles per `theme.breakpoints` key, object grammar — the viewport sibling of `cq` (see {@link ViewportMediaStyles}).

Besides the props above, every component accepts the layout props (spacing, color, flex/grid, size, border) — not repeated here.

children and description coexist in distinct areas — passing both does not make one replace the other. Also, don't wrap a string title in its own <Text>; the heading typography is already applied automatically.

See also

  • Storybook story: Components / Card
  • Base container: Flex.

Theming — surface ladder

Card sits on the surface ladder: it paints surface.paper, outlines with edge.hairline, and sets its title/description inks to ink.primary/ink.secondary. The roles are mode-aware by construction — paper is lighter than the canvas in both light and dark — and every ink inside resolves against this ground through the ground channel. Override a role per polarity with createTheme({ colors: { ladder: … } }); see Dark Mode Engine.