FairGarden Design

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.

Spring bird count

Editorial · bare (default)

Volunteers tally migrants along the river trail every May.
CardKinds.tsx
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

Feature · 12 min read

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.

Owl prowl

Sat 12 Oct · North kiosk

A short walk after dark.

Seed library open house

Sun 13 Oct · Barn

Bring seed from your garden, take seed home, and learn to clean and store what you saved this year.

Spring bird count

Past event

Unavailable: a dotted edge and muted text, the action removed.
CardLead.tsx
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

olive × green

Rules and text from olive, accents from green.

Royal pairing

primary="slate" secondary="indigo"

A verified pairing from the §2 matrix.

Clay pairing

primary="olive" secondary="orange"

Olive keeps the text and rules; orange takes the accents.
CardColor.tsx
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=&quot;slate&quot; secondary=&quot;indigo&quot;</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=&quot;olive&quot; secondary=&quot;orange&quot;</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

On paper

Owls, moths and the smell of wet leaves.

Night walk

On forest

Owls, moths and the smell of wet leaves.

Night walk

On leaf

Owls, moths and the smell of wet leaves.
CardGrounds.tsx
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).

PropTypeDescription
faced
boolean | undefined

Adds the face (a nested white Ground on a page ground, a paper light island inside a field or the night band) and its --role-edge edge. Editorial only. Default false: the open card (§12.2).

kind
'editorial' | 'block' | 'entry' | undefined

Editorial (default): open (bare) by default per §12.2; faced adds the face and edge.

lead
boolean | undefined

The single lead card of a grid: horizontal, media 5 columns and text 7, once its card-grid container reaches 944 px (viewport fallback --lg-n-above); stacked below. Editorial only. Default false.

primary
| 'ruby'
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| null
| undefined

Primary Radix scale for the card and its face: text, rules and edges. Never defaulted; omitted, it inherits the scope [D133].

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
| undefined

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
boolean | undefined

Unavailable (a sold-out product, a past event): title and body in --role-muted, a line-dotted-fine edge, the action removed; the photo is untouched and nothing fades [D16]. Add the status word as a Badge. Default false.

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.

PropTypeDescription
external
boolean | null | undefined

Adds the arrow-open mark and “(external site)” for assistive technology. Default false.

index
boolean | null | undefined

Adds the screen-only visited ✓ used in long indexes, such as reference and archive lists. Default false [D174, D175].

list
boolean | null | undefined

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.

muted
boolean | null | undefined

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.

primary
| 'ruby'
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| null
| undefined

Primary Radix scale: the link text and focus ring. Never defaulted; omitted, it inherits the scope [D133].

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
| undefined

Secondary Radix scale: the accent underline. Never defaulted; omitted, it inherits the scope.

className
string | undefined

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

Props for CardFooter: div props and render.

type CardFooterProps = useRender.ComponentProps<'div'>
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).