Card

Groups related content onto one bounded surface.

import { Card } from "@usefragments/ui";Source

Preview

<Card>
  <Card.Header>
    <Card.Title>Card title</Card.Title>
    <Card.Description>A brief description</Card.Description>
  </Card.Header>
  <Card.Body>Related content</Card.Body>

Usage

Use when

  • Grouping related content behind one boundary
  • Previews, summaries, and dashboard tiles
  • Separating sections that spacing alone cannot

Avoid when

  • Plain text that needs no grouping
  • Modal surfaces (use Dialog)
  • Navigation items (use List or Sidebar)

Implementation notes

  • One surface: the surface plane, the hairline, the surface corner, no shadow
  • Cards sharing a grid row should size uniformly
  • tone="danger" is the only state chrome; put other state in a Badge or Alert inside
  • padding="none" plus Card.Header divided and Card.Body padding is the panel
  • A feature mosaic is Grid.Item colSpan and rowSpan around a Card
  • Prefer a Button or Link inside the card; when the whole card acts, pass render

Do not use a Card as a modal surface.

Don’t

<Card>Confirm deletion</Card>

Do

<Card>
  <Card.Body>Inline summary</Card.Body>
</Card>

Installation

pnpm add @usefragments/ui

The npm package stays managed by your package manager. Import styles once from @usefragments/ui/styles. See the installation guide for framework and theme setup.

Anatomy

Import Card once, then compose its parts with dot notation.

  • Card.Header
  • Card.Title
  • Card.Description
  • Card.Body
  • Card.Footer

Quiet surface with a title and description.

<Card>
  <Card.Header>
    <Card.Title>Card title</Card.Title>
    <Card.Description>A brief description</Card.Description>
  </Card.Header>
  <Card.Body>Related content</Card.Body>
</Card>

Examples

Content Only

Body alone, when there is nothing to title.

<Card>
  <Card.Body>Just content, no header or footer.</Card.Body>
</Card>

Footer parks the actions below the content.

<Card>
  <Card.Header>
    <Card.Title>Card with footer</Card.Title>
    <Card.Description>Complete card layout</Card.Description>
  </Card.Header>
  <Card.Body>Main content area.</Card.Body>

Metric

A metric tile is the same surface: the figure on the display step, tabular.

<Card>
  <Card.Body>
    <Text as="p" type="display" tabularNums>
      94%
    </Text>
    <Text as="p" color="secondary">

Panel

A 40px divided head with the line below and no fill; the body owns its own spacing.

<Card padding="none">
  <Card.Header divided>
    <Card.Title>Overview</Card.Title>
  </Card.Header>
  <Card.Body padding="md">Panel content</Card.Body>
</Card>

Danger Tone

The only state chrome: the edge in the danger ink, no wash.

<Card tone="danger">
  <Card.Header>
    <Card.Title>Merge held</Card.Title>
    <Card.Description>Two blocking findings need a decision.</Card.Description>
  </Card.Header>
</Card>

Interactive

render makes the card a real link or button: gated hover tint, press scale, focus ring. Inside a button the parts draw as spans.

<Card render={<a href="#details" />}>
  <Card.Header>
    <Card.Title>Open details</Card.Title>
    <Card.Description>The whole card is one link</Card.Description>
  </Card.Header>
</Card>

Selected

The chosen card: the selection wash, and the hairline turns into the ring.

<Card selected render={<button type="button" aria-pressed="true" />}>
  <Card.Header>
    <Card.Title>Team plan</Card.Title>
    <Card.Description>Chosen</Card.Description>
  </Card.Header>
</Card>

Section Root

The as prop matches the surrounding document semantics.

<Card as="section" aria-labelledby="billing-card-title">
  <Card.Header>
    <Card.Title id="billing-card-title">Billing summary</Card.Title>
  </Card.Header>
  <Card.Body>Section semantics support the surrounding document outline.</Card.Body>
</Card>

Nested Heading

Pick the heading level the page outline expects.

<Card padding="none">
  <Card.Header divided>
    <Card.Title as="h4">Nested panel</Card.Title>
  </Card.Header>
  <Card.Body padding="md">Panel content</Card.Body>
</Card>

Long Content

Long titles and dense text wrap without breaking the surface.

<Card>
  <Card.Header>
    <Card.Title>
      A detailed localized account recovery policy for administrators across multiple
      workspaces
    </Card.Title>

API reference

Component props
Prop
Type
Default
Description
as"div" | "article" | "section" | undefineddivarticlesectionarticleRoot element tag for a static card.
Body.childrenRequiredReact.ReactNodeNot set—
Body.paddingCardPadding | undefinedmdnoneNot setInner padding for the body alone, with `padding="none"` on the root. Omit when the root already owns padding.
childrenRequiredReact.ReactNodeNot set—
Description.childrenRequiredReact.ReactNodeNot set—
Footer.childrenRequiredReact.ReactNodeNot set—
Header.childrenRequiredReact.ReactNodeNot set—
Header.dividedboolean | undefinedNot setA 40px head with the hairline below it and no fill, for a card with `padding="none"`. Title cluster and trailing controls pin to opposite edges.
paddingCardPadding | undefinedmdnonemdInner padding: `md` is the compact surface inset (12); `none` lets a divided header and `Card.Body` own their inset (a table sheet).
renderuseRender.RenderProp<Record<string, unknown>> | undefinedNot setMakes the card one interactive element: pass a real link or button, `render={<a href="/repos/web" />}` or `render={<button type="button" />}`. It gains the hover tint, the press scale and the focus ring. A button holds phrasing content only, so any render other than a plain `<a>` draws every part as a `span` (Title loses its heading level). Keep other controls out of an interactive card.
selectedboolean | undefinedfalseMarks the card as the chosen one: the selection wash, and the hairline turns into the selection ring. Pair it with the ARIA state that fits the element (`aria-pressed` on a button, `aria-current` on a link).
Title.as"h2" | "h3" | "h4" | "h5" | "h6" | undefinedh2h3h4h5h6Not setSemantic heading level.
Title.childrenRequiredReact.ReactNodeNot set—
toneCardTone | undefineddangerneutralneutral`danger` draws the edge in the danger ink, for a card whose content is held (a blocked merge). There is no wash; the words inside say why.

Accessibility

  • Card is a semantic container (article/div/section); render={<a href />} or render={<button />} makes it one real interactive element
  • A selected card pairs selected with aria-pressed on a button or aria-current on a link
  • Card titles should be appropriate heading levels
  • GridparentUse Grid + Card for responsive card layouts
  • ListalternativeUse List for linear, text-first layouts

Next steps