Card
A self-contained, linked item among peers, composed from semantic HTML with optional parts for media, title, meta, body and footer.
import {
Card,
CardBody,
CardMeta,
CardTitle,
CardTitleLink,
} from '@fairgarden-private/design/components/Card'
CardTitleLink is the card's one primary link: its hit area stretches over the whole card, and the card draws the focus ring outside its frame. The root is an inline-size container, so give it a width in shrink-to-fit layouts.
Kinds
Kinds
Editorial cards are bare by default; faced adds a face and an edge. kind="block" is the featured block-edge card, one family per page. kind="entry" is the hero's compact entry card: a white face with a 2 px --primary12 edge, the square CardMedia image flush to its start edge and as tall as the card, then a CardKicker category in caps and the title link. It stays horizontal at every width; the Hero lays up to three of them across its bottom edge.
Protect the headwaters
import {
Card,
CardBody,
CardFooter,
CardKicker,
CardMedia,
CardMeta,
CardTitle,
CardTitleLink,
} from '@fairgarden-private/design/components/Card'
import { Link } from '@fairgarden-private/design/components/Link'
import styles from './kinds.module.css'
/** A flat square placeholder (inline SVG), standing in for a photograph in this demo. */
const square =
'data:image/svg+xml;utf8,' +
encodeURIComponent(
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120">' +
'<rect width="120" height="120" fill="#b8c4a8"/>' +
'<path d="M0 88 34 52l24 20 20-14 42 34v28H0z" fill="#5f6f4e"/>' +
'</svg>'
)
export function CardKinds() {
return (
<div className={styles.grid}>
<Card>
<CardTitle>
<CardTitleLink href="#kinds">Spring bird count</CardTitleLink>
</CardTitle>
<CardMeta>Editorial · bare (default)</CardMeta>
<CardBody>Volunteers tally migrants along the river trail every May.</CardBody>
</Card>
<Card faced>
<CardTitle>
<CardTitleLink href="#kinds">Meadow restoration</CardTitleLink>
</CardTitle>
<CardMeta>Editorial · faced</CardMeta>
<CardBody>Three seasons of native seed, burning and patience.</CardBody>
<CardFooter>
<Link kind="standalone" href="#kinds">
Read more
</Link>
</CardFooter>
</Card>
<Card kind="block">
<CardTitle>
<CardTitleLink href="#kinds">Protect the headwaters</CardTitleLink>
</CardTitle>
<CardMeta>Block · featured</CardMeta>
<CardBody>The block edge marks a featured card: one family per page.</CardBody>
</Card>
<Card kind="entry">
<CardMedia>
<img src={square} alt="" />
</CardMedia>
<CardKicker>Entry · hero</CardKicker>
<CardTitle render={<p />}>
<CardTitleLink href="#kinds">Find a land trust</CardTitleLink>
</CardTitle>
</Card>
</div>
)
}
Lead, footers and unavailable
Lead, pinned footers and unavailable
lead makes the one lead card of a grid horizontal once its card-grid container reaches 944 px: the media takes 5 of 12 columns and the text the other 7, and a faced card's photo runs flush down its start edge. Below that it stacks like any card. Cards in a grid row share one height, and CardFooter is pinned to the bottom, so a row's actions line up. disabled marks an unavailable card (a sold-out product, a past event): a dotted edge, muted title and body, and no action. Say why in words, with a Badge. CardChoice holds an option card's Checkbox or Radio; when it's checked the edge steps to 2 px in the selection edge color.
The river comes back
Owl prowl
Seed library open house
Spring bird count
import { Button } from '@fairgarden-private/design/components/Button'
import {
Card,
CardBody,
CardFooter,
CardMedia,
CardMeta,
CardTitle,
CardTitleLink,
} from '@fairgarden-private/design/components/Card'
import styles from './lead.module.css'
const photo =
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 3 2'%3E%3Crect width='3' height='2' fill='%23b7c4a8'/%3E%3Cpath d='M0 1.4 .9.9l.7.4.8-.6.6.5V2H0z' fill='%23627a55'/%3E%3C/svg%3E"
/** The lead card goes horizontal from 944 px of its grid; cards in a row share one height, footers pinned. */
export function CardLead() {
return (
<div className={styles.stack}>
<div className={styles.lead}>
<Card faced lead>
<CardMedia>
{/* A plain data-URI stand-in photo; next/image adds nothing here. */}
<img src={photo} alt="" />
</CardMedia>
<CardTitle>
<CardTitleLink href="#lead">The river comes back</CardTitleLink>
</CardTitle>
<CardMeta>Feature · 12 min read</CardMeta>
<CardBody>
Ten years after the dam came out, the shad run is the largest on record and the
floodplain forest is filling in on its own.
</CardBody>
<CardFooter>
<Button size="sm">Read the Story</Button>
</CardFooter>
</Card>
</div>
<div className={styles.row}>
<Card faced>
<CardTitle>
<CardTitleLink href="#lead">Owl prowl</CardTitleLink>
</CardTitle>
<CardMeta>Sat 12 Oct · North kiosk</CardMeta>
<CardBody>A short walk after dark.</CardBody>
<CardFooter>
<Button size="sm">Get Tickets</Button>
</CardFooter>
</Card>
<Card faced>
<CardTitle>
<CardTitleLink href="#lead">Seed library open house</CardTitleLink>
</CardTitle>
<CardMeta>Sun 13 Oct · Barn</CardMeta>
<CardBody>
Bring seed from your garden, take seed home, and learn to clean and store what you
saved this year.
</CardBody>
<CardFooter>
<Button size="sm">Get Tickets</Button>
</CardFooter>
</Card>
<Card faced disabled>
<CardTitle>
<CardTitleLink href="#lead">Spring bird count</CardTitleLink>
</CardTitle>
<CardMeta>Past event</CardMeta>
<CardBody>Unavailable: a dotted edge and muted text, the action removed.</CardBody>
<CardFooter>
<Button size="sm">Get Tickets</Button>
</CardFooter>
</Card>
</div>
</div>
)
}
Primary and secondary
Primary and secondary
primary and secondary pass through to the card's face scope, so rules, text and the title's accent re-resolve together.
Scope defaults
Royal pairing
Clay pairing
import {
Card,
CardBody,
CardMeta,
CardTitle,
CardTitleLink,
} from '@fairgarden-private/design/components/Card'
import styles from './color.module.css'
/** The props pass through to the card's face scope. */
export function CardColor() {
return (
<div className={styles.grid}>
<Card faced>
<CardTitle>
<CardTitleLink href="#color">Scope defaults</CardTitleLink>
</CardTitle>
<CardMeta>olive × green</CardMeta>
<CardBody>Rules and text from olive, accents from green.</CardBody>
</Card>
<Card faced primary="slate" secondary="indigo">
<CardTitle>
<CardTitleLink href="#color">Royal pairing</CardTitleLink>
</CardTitle>
<CardMeta>primary="slate" secondary="indigo"</CardMeta>
<CardBody>A verified pairing from the §2 matrix.</CardBody>
</Card>
<Card faced primary="olive" secondary="orange">
<CardTitle>
<CardTitleLink href="#color">Clay pairing</CardTitleLink>
</CardTitle>
<CardMeta>primary="olive" secondary="orange"</CardMeta>
<CardBody>Olive keeps the text and rules; orange takes the accents.</CardBody>
</Card>
</div>
)
}
On grounds
On paper, forest and leaf
A faced card nests its own Ground: white on a page ground, and a paper light island inside a field (forest, leaf) or the night band. Its edge is that face's own edge color, so it stays visible on every ground.
Night walk
Night walk
Night walk
import {
Card,
CardBody,
CardMeta,
CardTitle,
CardTitleLink,
} from '@fairgarden-private/design/components/Card'
import { PresetGround } from '@/components/PresetGround'
import styles from './grounds.module.css'
const presets = ['paper', 'forest', 'leaf'] as const
/** A faced card is a white face on a page ground and a paper light island inside a field. */
export function CardGrounds() {
return (
<div className={styles.row}>
{presets.map((preset) => (
<PresetGround key={preset} preset={preset} className={styles.band}>
<Card faced>
<CardTitle>
<CardTitleLink href="#grounds">Night walk</CardTitleLink>
</CardTitle>
<CardMeta>On {preset}</CardMeta>
<CardBody>Owls, moths and the smell of wet leaves.</CardBody>
</Card>
</PresetGround>
))}
</div>
)
}
API Reference
Card
The card frame. Plain children are fine (<Card>…</Card>); the optional
parts CardMedia, CardKicker, CardTitle (+ CardTitleLink),
CardMeta, CardBody, CardChoice and CardFooter give the §12.2
anatomy and rhythm. Put a
leading CardMedia first (a direct child) to run it flush to the frame.
In a grid row, cards share one height and the footer sits at the bottom.
Renders <article>; pass render={<li />} or similar to change it. The
root is an inline-size container, so give it a width in shrink-to-fit
contexts (flex items without stretch, floats).
| Prop | Type | Description |
|---|---|---|
faced | | Adds the face (a nested |
kind | | Editorial (default): open (bare) by default per §12.2; |
lead | | The single lead card of a grid: horizontal, media 5 columns and
text 7, once its |
primary | | Primary Radix scale for the card and its face: text, rules and edges. Never defaulted; omitted, it inherits the scope [D133]. |
secondary | | Secondary Radix scale for the card and its face: the accents, such as the title link’s underline. Never defaulted; omitted, it inherits the scope. |
disabled | | Unavailable (a sold-out product, a past event): title and body in
|
CardTitle and CardTitleLink
type-itemhead in --primary12. Renders <h3>; pass render={<h2 />} to fit the outline.
The card’s one primary link, inside CardTitle. A §9.3 title Link: its
hit area stretches over the whole card; hover, focus and press underline
the title; the card draws the focus ring outside its frame.
| Prop | Type | Description |
|---|---|---|
external | | Adds the arrow-open mark and “(external site)” for assistive technology.
Default |
index | | Adds the screen-only visited ✓ used in long indexes, such as reference
and archive lists. Default |
list | | With |
muted | | Rests in |
primary | | Primary Radix scale: the link text and focus ring. Never defaulted; omitted, it inherits the scope [D133]. |
secondary | | Secondary Radix scale: the accent underline. Never defaulted; omitted, it inherits the scope. |
className | | Extra class names, added after the module’s own. |
CardMeta, CardBody and CardFooter
Date, format, place joined by “·": type-caption in --role-muted.
Two to four lines of type-body-ui in --primary12.
The action row (§12.2 part action): a sm outline Button, or a
“READ MORE ›” standalone Link. Pinned to the card’s bottom, so a grid
row’s actions align (§12.3); sits above the stretched title link; stacks
below 360 px of card width; removed while the card is disabled.
CardChoice
The option-card indicator slot (§12.2 part choice, §10.7): put the
card’s Checkbox or Radio here. Its data-checked steps the faced edge to
--border-size-2 in --role-select-edge; its data-disabled draws the
unavailable edge and mutes the title and body. Sits above the stretched
title link as its own control.
CardMedia and CardKicker
Photo or drawing (figure with an img). Flush when first in a faced card; inset in a block card.
The kicker above the title (§12.2 part kicker): a topic Tag, or a
category in type-label caps in --primary12, as on the entry card.
Additional types
CardBodyProps
Props for CardBody: div props and render.
type CardBodyProps = useRender.ComponentProps<'div'>CardChoiceProps
Props for CardChoice: div props and render.
type CardChoiceProps = useRender.ComponentProps<'div'>CardFooterProps
CardKickerProps
Props for CardKicker: paragraph props and render.
type CardKickerProps = useRender.ComponentProps<'p'>CardMediaProps
Props for CardMedia: figure props and render.
type CardMediaProps = useRender.ComponentProps<'figure'>CardMetaProps
Props for CardMeta: paragraph props and render.
type CardMetaProps = useRender.ComponentProps<'p'>CardProps
Props for Card: article props, render, and the kind, face and color axes.
type CardProps =
| (React.ClassAttributes<HTMLElement> &
React.HTMLAttributes<HTMLElement> &
__type & {
primary?:
| 'ruby'
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| null;
secondary?:
| 'ruby'
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| 'amber'
| 'blue'
| 'cyan'
| 'grass'
| 'green'
| 'jade'
| 'lime'
| 'mint'
| 'orange'
| 'sky'
| 'teal'
| 'yellow'
| null;
disabled?: boolean;
} & { kind?: 'editorial'; faced?: boolean; lead?: boolean })
| (React.ClassAttributes<HTMLElement> &
React.HTMLAttributes<HTMLElement> &
__type & {
primary?:
| 'ruby'
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| null;
secondary?:
| 'ruby'
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| 'amber'
| 'blue'
| 'cyan'
| 'grass'
| 'green'
| 'jade'
| 'lime'
| 'mint'
| 'orange'
| 'sky'
| 'teal'
| 'yellow'
| null;
disabled?: boolean;
} & { kind: 'block'; faced?: undefined; lead?: undefined })
| (React.ClassAttributes<HTMLElement> &
React.HTMLAttributes<HTMLElement> &
__type & {
primary?:
| 'ruby'
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| null;
secondary?:
| 'ruby'
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| 'amber'
| 'blue'
| 'cyan'
| 'grass'
| 'green'
| 'jade'
| 'lime'
| 'mint'
| 'orange'
| 'sky'
| 'teal'
| 'yellow'
| null;
disabled?: boolean;
} & { kind: 'entry'; faced?: undefined; lead?: undefined })CardTitleLinkProps
Props for CardTitleLink: Link props except kind, which is always title.
type CardTitleLinkProps = {
/** Extra class names, added after the module's own. */
className?: string;
/**
* Primary Radix scale: the link text and focus ring. Never defaulted;
* omitted, it inherits the scope [D133].
*/
primary?:
| 'ruby'
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| null;
/**
* Secondary Radix scale: the accent underline. Never defaulted; omitted,
* it inherits the scope.
*/
secondary?:
| 'ruby'
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| 'amber'
| 'blue'
| 'cyan'
| 'grass'
| 'green'
| 'jade'
| 'lime'
| 'mint'
| 'orange'
| 'sky'
| 'teal'
| 'yellow'
| null;
/**
* With `kind="nav"`: a list link (nav-panel, footer, drawer and breadcrumb
* lists), whose hover is `--role-link-hover` color only, plus the
* `--ds-stroke-1-5` `--role-accent` underline where that ink is
* `--primary12`. Without it, `nav` is bare navigation text (bar and
* utility items, page numbers, toolbar links), whose hover is the
* `--border-size-2` `--role-accent` underline (§9.3) [D181]. Default
* `false`.
*/
list?: boolean | null;
/**
* Adds the screen-only visited ✓ used in long indexes, such as reference
* and archive lists. Default `false` [D174, D175].
*/
index?: boolean | null;
/**
* Adds the arrow-open mark and "(external site)" for assistive technology.
* Default `false`.
*/
external?: boolean | null;
/**
* Rests in `--role-muted` instead of `--primary12`, as a breadcrumb's
* ancestors. Hover takes `--role-link-hover` only (the underline is added
* where that ink is `--primary12`); with `kind="nav"` it replaces the bar
* item's bare-text underline (§9.3, §9.8) [D181]. Default `false`.
*/
muted?: boolean | null;
}CardTitleProps
Props for CardTitle: heading props and render (default <h3>).
type CardTitleProps = useRender.ComponentProps<'h3'>Specification: DESIGN-SYSTEM.md §12.2 (card).