Card
Flexible content container with composable subcomponents and a layout axis for four anatomy-driven arrangements.
Card is the canonical content-grouping container. A default flexible mode, plus four arrangements selected by the layout prop:
- Default — flexible container, compose with
CardTitle/CardDescription/CardFootersubcomponents. layout="stack"— vertical content card and the malleable cell of a card-grid Section.layout="row"— horizontal content card, media left and text right.layout="metric"— compact stat tile, label above a large value.layout="control"— settings/integration row, media + text left, status + action right.layout="pricing"— vertical pricing tier, price + period block above a feature checklist.
One flat slot set (media / overline / title / children / action / badge) serves all four, per ADR-038.
The preset prop has been removed. preset="control" | "summary" | "display" | "display-row" is replaced by layout (ADR-038 §5). See Migrating off preset for the mapping.
Use it for
- Service grids, feature cards, and content blocks (
layout="stack") - Settings panels with title + description + toggle/action (
layout="control") - Dashboard stat tiles ($48K Q1 revenue, 142 active users) (
layout="metric") - Pricing tiers and plan comparison grids (
layout="pricing") - Any grouped-content surface where consistent border/padding/shadow matter
For a horizontal stack of card-like rows, use FieldGrid wrapping Cards. For loading-state placeholders, use Skeleton variant="rectangular".
Import
import { Card, CardTitle, CardDescription, CardFooter } from '@brikdesigns/bds';Default mode
Compose freely with the Title / Description / Footer subcomponents. Omit layout entirely.
<Card variant="outlined" padding="md">
<CardTitle>Title</CardTitle>
<CardDescription>Description text goes here.</CardDescription>
<CardFooter>
<Button variant="primary" size="sm">Action</Button>
</CardFooter>
</Card>Variants
outlined(default) — Subtle secondary border.brand— Primary-color border for emphasis.elevated— Surface-primary fill, no border, no shadow (flat since brik-bds#1146; for a shadow useraised).raised— Surface-primary fill, no border, with a cast--box-shadow-mddrop shadow. The shadow-casting counterpart to the now-flatelevated; use for a focal/lone card or a grid cell that needs a lifted, contained read.borderless— Transparent fill, no border, no shadow. Use for cards sitting on a colored surface, where the border ring reads as visual noise.
The layout arrangements take the grid-cell subset — borderless, elevated, raised — so one option set covers every surface treatment.
Padding
none, sm, md (default), lg. Match content density.
Interactive + link
interactive adds hover affordance. href renders as an <a> so the whole card becomes a link.
<Card variant="elevated" interactive>
Hover me
</Card>
<Card href="/articles/foo" interactive>
Whole card is a link
</Card>Leading media
In default mode the media prop takes a CardMedia object and turns the card into a horizontal "media object" — an Avatar, a square 1:1 Image, or a bundled Logo on the left, with children stacked to the right. Pass exactly one of avatar / image / logo. All size to the shared Avatar scale (sm 32px, md 40px, lg 48px, xl 64px). The avatar falls back to initials from name and can carry a presence status; the image takes fit (contain for logos, cover for photos).
<Card media={{ avatar: { name: 'Jordan Lee', status: 'online', size: 'lg' } }}>
<CardTitle as="h4">Jordan Lee</CardTitle>
<CardDescription>jordan.lee@brikdesigns.com</CardDescription>
</Card>
<Card media={{ image: { src: '/brik-logo.svg', alt: 'Brik Designs logo', fit: 'contain', size: 'lg' } }}>
<CardTitle as="h4">Brik Designs</CardTitle>
<CardDescription>Design system · Enterprise plan</CardDescription>
</Card>Under a layout, media is a plain ReactNode instead — pass the <Frame>, <Avatar>, <Logo>, or <ServiceTag> you want rendered.
Stack layout
layout="stack" is the vertical content card and the malleable cell of a card-grid Section. Every slot is optional and independently toggled, so one arrangement serves any content type — service, blog post, customer story, property listing, team bio, support plan.
<Card
layout="stack"
overline={<ServiceTag line="brand" />}
media={<Frame ratio="16-9"><img src="/service.jpg" alt="" /></Frame>}
badge={<Badge>Featured</Badge>}
title="Brand identity"
action={<Button href="/services/branding" variant="outline" size="sm">Learn more</Button>}
>
Logo, type, and color systems built to scale.
</Card>title renders as titleAs (default h3) — set it to keep the document outline correct under a grid-section heading; the visual size is token-driven and unchanged by the level. For a cell on a colored (service-tinted) grid, variant="borderless" (transparent), variant="elevated" (fill + shadow), or variant="raised" (fill + cast --box-shadow-md shadow) swaps the surface treatment, and tint applies a pale service-line wash. badge renders only when media is also set.
mediaTreatment controls how the media slot meets the card edge. flush (default) bleeds the media to the edge and pads only the body — the blog/story/product-grid look. inset frames media and body together in a single inset — the service-card look.
Row layout
layout="row" is the horizontal content card and card-grid section row — media on the left, the text column (overline, title, body, action) on the right. Use for single-row sections where a vertical layout wastes horizontal space: Related Customer Story, Recommended Add-On, featured plan. Collapses to a vertical stack at ≤ 640px.
<Card
layout="row"
overline={<Tag>Case study</Tag>}
media={<Frame ratio="4-3"><img src="/story.jpg" alt="" /></Frame>}
imageWidth="standard"
title="How Northwind tripled conversions"
action={<Button href="/stories/northwind" variant="outline" size="sm">Read the story</Button>}
>
A ground-up rebrand and site rebuild.
</Card>imageWidth sets the left column: narrow (25%), standard (35%, default), or wide (50%) — or pass any CSS length/percentage string ("40%", "320px").
Metric layout
layout="metric" is the compact stat tile — an overline label above a large title value, with an optional trailing action. Context-neutral: a revenue figure, an active-user count, a storage total. Pre-format the value; the card applies no numeric formatting.
<Card
layout="metric"
overline="Q1 revenue"
title="$48,250.75"
action={<TextLink href="/revenue">Details</TextLink>}
/>Two optional slots extend metric for a labelled selection — the shape the removed ProductSummaryCard component covered before it folded into this layout (ADR-038 §4). media renders a leading glyph beside the stat, and detail renders a secondary line below the value. Both are additive, so a plain stat omits them.
<Card
layout="metric"
media={<ServiceTag category="brand" variant="icon" size="lg" serviceName="Logo Design" />}
overline="Interested in"
title="Standard Logo Design"
detail="$650 • one time"
/>Control layout
layout="control" is the canonical settings and integration row. Leading media (logo) plus title and description on the left; connectionStatus indicator and action on the right.
<Card
layout="control"
media={<Logo set="integration" name="notion" />}
title="Email notifications"
description="Send a weekly digest to your inbox."
action={<Button variant="outline" size="sm">Configure</Button>}
/>actionAlign controls vertical alignment of the trailing block — center (default) or top, which anchors to the upper-right when descriptions are tall. connectionStatus accepts not-configured, linked (linked, nothing read yet: neutral, never green), connected, syncing, synced, or failed and renders a status indicator alongside the action; pass lastSynced for a timestamp beneath it.
layout="control" replaces the standalone <CardControl> component, which is deprecated per ADR-004 (#657). New code should use the layout; see the CardControl migration guide.
Pricing layout
layout="pricing" is the vertical pricing tier — optional top media, a badge + title header, the price / period block, a children body, a divider, the features checklist, and a bottom-anchored action.
<Card
layout="pricing"
title="Professional"
price="$49"
period="/month"
badge={<Badge status="positive">Most popular</Badge>}
features={['Unlimited projects', 'Priority support', 'Custom domain']}
action={<Button variant="primary" size="sm">Get started</Button>}
>
<CardDescription>For growing businesses.</CardDescription>
</Card>price, period and features are this layout's own slots, the same shape as control's connectionStatus / lastSynced / actionAlign. Pre-format the price — the card applies no numeric formatting. Omit features (or pass []) to drop the checklist and its divider.
Mark the recommended tier with variant="raised" or a tint. There is deliberately no highlighted prop: the surface set is shared across every layout, so a per-layout highlight modifier would re-declare what variant already owns.
layout="pricing" supersedes the standalone <PricingCard>, which is deprecated as a standalone surface (#2571). See ADR-038 § Amendment 2026-09-18 for why the pricing anatomy earns a layout rather than a stack recipe.
Migrating off preset
The preset discriminator predated ADR-038 and has been removed (ADR-038 §5). Use layout; the mapping below is for updating any code still on the old union.
| Removed | Replacement | Slot mapping |
|---|---|---|
preset="display" | layout="stack" | image → media, tag → overline, description → children |
preset="display-row" | layout="row" | same as stack, laid out horizontally |
preset="summary" | layout="metric" | label → overline, value → title; pre-format the value |
preset="control" | layout="control" | logo → media, other slots unchanged |
<PricingCard> | layout="pricing" | image → media, description → children; highlighted → variant="raised" |
<ProductSummaryCard> | layout="metric" | media = <ServiceTag>, overline = label, title = value, detail = price • frequency |
Two behavior notes on the way across:
preset="summary"formatted numbers for you viatype="numeric" | "price".layout="metric"does not — pass an already-formatted string.varianthad two option sets under the union (the default shape's five, the display presets' three). Underlayoutthere is one set; the arrangements acceptborderless,elevated, andraised.
When not to use
- Don't use Card for inline content blocks. Cards imply discrete grouping; for paragraphs and section dividers, use prose + Divider.
- Don't use Card for tables. Use a table component when rows share columns. Cards are for self-contained groupings.
- Don't reach for the default mode when a
layoutfits. The layouts carry locked-down anatomy that propagates consistently across screens.
Accessibility
- Default Card renders a
<div>; withhref, it renders an<a>with full anchor semantics (right-click, keyboard, screen-reader link role). interactivewithouthrefaddsrole="button"only whenonClickis also passed; otherwise it's a non-interactive hover affordance.titlerenders astitleAs(defaulth3) under alayout, and as an<h3>in theCardTitlesubcomponent — adjust the surrounding heading hierarchy accordingly.
API
Default mode
| Prop | Type | Default |
|---|---|---|
variant | 'outlined' | 'brand' | 'elevated' | 'raised' | 'borderless' | 'outlined' |
padding | 'none' | 'sm' | 'md' | 'lg' | 'md' |
interactive | boolean | false |
href | string | — |
media | CardMedia (exactly one of avatar / image / logo) | — |
children | ReactNode (required) | — |
Layout modes
One flat slot set serves all four arrangements. A slot that a layout does not render is ignored rather than type-rejected — the per-layout notes below the table say which is which.
| Prop | Type | Default |
|---|---|---|
layout | 'stack' | 'row' | 'metric' | 'control' | 'pricing' (required) | — |
media | ReactNode | — |
overline | ReactNode | — |
title | ReactNode | — |
titleAs | 'h2' | 'h3' | 'h4' | 'h3' |
children | ReactNode | — |
action | ReactNode | — |
badge | ReactNode | — |
variant | 'borderless' | 'elevated' | 'raised' | — |
tint | CardTint (service line) | — |
href | string | — |
imageWidth | CardDisplayRowImageWidth (narrow / standard / wide, or any CSS length) | 'standard' |
mediaTreatment | 'flush' | 'inset' | 'flush' |
description | ReactNode | — |
actionAlign | 'center' | 'top' | 'center' |
connectionStatus | 'not-configured' | 'linked' | 'connected' | 'syncing' | 'synced' | 'failed' | — |
lastSynced | string | — |
detail | ReactNode | — |
Slots by layout:
| Layout | Renders |
|---|---|
stack | media, overline, title, children, action, badge, tint, variant, mediaTreatment, href |
row | the stack set plus imageWidth, minus mediaTreatment |
metric | media, overline, title, detail, action |
control | media, title, description, action, actionAlign, connectionStatus, lastSynced |
All modes accept standard <div> HTML attributes (excluding title, which is repurposed by the layouts and the control layout).
Related
- CardControl — deprecated; see
layout="control" - CardList — list-of-cards layout
- Testimonial — testimonial component (Storybook; renamed from CardTestimonial)
- CollapsibleCard — Card + Accordion mash-up
- PricingCard — pricing tier card
- Divider — for non-card section separators
- FieldGrid — equal-column wrapper for stat tiles
- Storybook playground