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… |
|---|---|
|
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.
<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.