opsinjs
ComponentsSurfaces

Card

A bounded group of related content with a border and consistent padding is the ordinary box that most of a screen is made of.

Preview

Theme
Density
Text
Status
card · base base · style base-lyraOpen under the product theme

Installation

pnpm dlx shadcn@latest add @opsinjs/card

The @opsinjs namespace is declared in your components.json. Everything it installs is code you then own. There is no runtime package to keep in step.

Usage

import { Card } from "@/components/ui/card"
<Card>
  <Card.Header title={<h3>Recent readings</h3>} />
  <Card.Body>Card content</Card.Body>
</Card>

When to use it

Use it when

  • Grouping content that belongs together and needs a visible boundary.
  • Giving a block its own padding, radius and rung so it reads as one thing, not a region of the page.
  • As the base for a purpose-built card with its own contract, the way ResultCard and CareCard are.

Do not use it when

  • The content is a health result with a value, range, status and meaning.Use result-card instead.
  • The content is an instruction the reader must act on.Use care-card instead.
  • The content is one measurement in a dashboard grid.Use metric-tile instead.
  • You need a layer over scrolling content that must stay readable.Use surface instead.
  • You want a border for advisory prose.Use callout instead.

Anatomy

  1. CardThe root: a div, or an anchor when href is set. Carries the corner and print boundary.Controlled by href
  2. Card.HeaderOptional. An exported part the consumer fills.
  3. Card.TitleHolds the caller's heading element and resets its font and margin.Controlled by title
  4. Card.DescriptionOptional line under the title. A paragraph, so inline content only.Controlled by description
  5. Card.BodyOptional. The content, no contract of its own.
  6. Card.FooterOptional. Actions or metadata in a wrapping row, split by the target minimum.
  • Card 1
    • Surface 1
      • Surface.Content 1
        • card-link-row 0..1
          • Card.Header 0..1
            • Card.Title 1
            • Card.Description 0..1
            • children 0..n
          • Card.Body 0..1
          • Card.Footer 0..1

Examples

href makes the card one anchor and one tab stop. A chevron marks it at rest, the title underlines on hover, and it holds no buttons.

Theme
Density
Text
Status
card-as-a-link · base base · style base-lyraOpen under the product theme

Cards on a sheet, and the rung that stays put

The sheet is the layer; the cards on it stay on the card rung, since a translucent rung never contains one.

Theme
Density
Text
Status
card-on-a-sheet · base base · style base-lyraOpen under the product theme

Content guidelines

A card holds one idea. If the title needs and, it is two cards. Titles are noun phrases in sentence case. Card ships no copy, so every word inside is the product's, held to Plain-English A to Z.

Do

"Recent readings" as a card title, with the readings inside it.

Don’t

"Recent readings and upcoming appointments" in one card. The boundary then tells the reader nothing.

Accessibility

Audited against WCAG 2.2 AA across a source pass and a rendered pass. The audit is author-run, not an independent review, and clinical review is still pending.

Checked by pnpm run check:a11y on every commit. Every colour is a role token, no type size is in px, and no banned word appears.

What the audit changed. The link card's focus-visible ring used a hardcoded 2px outline instead of the house tokens. It now reads --opsin-border-focus and --opsin-border-focus-offset, so a product raising those gets a wider ring here; the default stays 2px. The rendered pass found no axe violation and no 320px reflow.

What it left standing. A link card is one anchor, so its accessible name is all its text at once, a verbosity to weigh before putting href on a long card. The resting chevron is aria-hidden, backed by the title underline and anchor as redundant cues. A card otherwise takes no role or name unless it is a link, carries the page's heading level in a title slot that resets font and margin, floors a link target at --opsin-target-minimum, and keeps its edge under greyscale, forced-colors and print.

KeyActionNotes
TabMoves to the card, when the card is a linkOne stop per card. A card without href is not focusable.
Shift+TabMoves backwards to the cardReverse order matches visual order.
EnterFollows the link, on a card that has an hrefThe root is a real anchor, so this is browser behaviour.
SpaceScrolls the pageA link activates on Enter, not Space.
PairThemeAPCA LcWCAG 2.2Floor
body text on the pagelight101.617.18:1Pass
body text on the pagedark-100.518.00:1Pass
body text on a cardlight104.717.96:1Pass
body text on a carddark-99.616.32:1Pass
body text on the muted groundlight98.116.32:1Pass
body text on the muted grounddark-97.513.77:1Pass
secondary text on the pagelight83.97.42:1Pass
secondary text on the pagedark-80.713.44:1Pass
secondary text on a cardlight87.07.76:1Pass
secondary text on a carddark-79.812.19:1Pass
secondary text on the muted groundlight80.47.05:1Pass
secondary text on the muted grounddark-77.810.28:1Pass
a hairline boundary on the pagelight19.31.41:1Below floor
a hairline boundary on the pagedark-8.21.90:1Below floor
a hairline boundary on a cardlight22.41.47:1Below floor
a hairline boundary on a carddark-7.31.72:1Below floor
a hairline boundary on the muted groundlight15.81.34:1Below floor
a hairline boundary on the muted grounddark0.01.45:1Below floor
a placeholder boundary on the pagelight69.84.52:1Pass
a placeholder boundary on the pagedark-51.07.62:1Pass
a placeholder boundary on a cardlight72.84.72:1Pass
a placeholder boundary on a carddark-50.16.91:1Pass
a placeholder boundary on the muted groundlight66.24.29:1Pass
a placeholder boundary on the muted grounddark-48.15.83:1Pass
a placeholder fill on the pagelight8.31.18:1Below floor
a placeholder fill on the pagedark-8.21.90:1Below floor
a placeholder fill on a cardlight11.41.23:1Below floor
a placeholder fill on a carddark-7.31.72:1Below floor
a placeholder fill on the muted groundlight0.01.12:1Below floor
a placeholder fill on the muted grounddark0.01.45:1Below floor
a placeholder boundary on its own filllight58.93.84:1Pass
a placeholder boundary on its own filldark-41.54.01:1Below floor
the card hairline on the pagelight69.84.52:1Pass
the card hairline on the pagedark-28.74.19:1Below floor
the card hairline on a cardlight72.84.72:1Pass
the card hairline on a carddark-27.93.80:1Below floor
body text on the canvas materiallight104.717.96:1Pass
body text on the canvas materialdark-100.518.00:1Pass
body text on the inset materiallight98.116.32:1Pass
body text on the inset materialdark-100.117.18:1Pass
body text on the card materiallight104.717.96:1Pass
body text on the card materialdark-99.616.32:1Pass
body text on the raised materiallight104.717.96:1Pass
body text on the raised materialdark-97.513.77:1Pass
body text on the sheet material over the darkest backdroplight99.916.76:1Pass
body text on the sheet material over the darkest backdropdark-99.716.47:1Pass
body text on the sheet material over the lightest backdroplight104.717.96:1Pass
body text on the sheet material over the lightest backdropdark-80.86.07:1Pass
body text on the overlay material over the darkest backdroplight85.913.48:1Pass
body text on the overlay material over the darkest backdropdark-99.916.77:1Pass
body text on the overlay material over the lightest backdroplight104.717.96:1Pass
body text on the overlay material over the lightest backdropdark-54.22.69:1Below floor

These are the measured token pairs this component draws colour from, not a measurement of the component itself.

API reference

Prop

Type

Generated from CardProps in registry/bases/base/card.tsx.

rung defaults to card, not raised, and never goes above raised: a translucent rung warns in development and renders. render takes the router's link element, not a function, and does nothing without href. There is no status, variant, color or category.

Prop

Type

Generated from CardHeaderProps in registry/bases/base/card.tsx.

Prop

Type

Generated from CardBodyProps in registry/bases/base/card.tsx.

Prop

Type

Generated from CardFooterProps in registry/bases/base/card.tsx.

  • ResultCard is a Card with a clinical contract, any card holding a health value.
  • CareCard is a Card whose subject is an authored instruction.
  • MetricTile has a fixed grid contract, not a small Card.
  • Surface is the layer beneath; Card is a Surface with padding and a boundary.
  • Callout is bounded advisory prose, not grouped content.

On this page