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
card · base base · style base-lyraOpen under the product themeInstallation
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-cardinstead. - The content is an instruction the reader must act on.Use
care-cardinstead. - The content is one measurement in a dashboard grid.Use
metric-tileinstead. - You need a layer over scrolling content that must stay readable.Use
surfaceinstead. - You want a border for advisory prose.Use
calloutinstead.
Anatomy
CardThe root: a div, or an anchor when href is set. Carries the corner and print boundary.Controlled byhrefCard.HeaderOptional. An exported part the consumer fills.Card.TitleHolds the caller's heading element and resets its font and margin.Controlled bytitleCard.DescriptionOptional line under the title. A paragraph, so inline content only.Controlled bydescriptionCard.BodyOptional. The content, no contract of its own.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
- Card.Header 0..1
- card-link-row 0..1
- Surface.Content 1
- Surface 1
Examples
The whole card as one link
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.
card-as-a-link · base base · style base-lyraOpen under the product themeCards 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.
card-on-a-sheet · base base · style base-lyraOpen under the product themeContent 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.
| Key | Action | Notes |
|---|---|---|
| Tab | Moves to the card, when the card is a link | One stop per card. A card without href is not focusable. |
| Shift+Tab | Moves backwards to the card | Reverse order matches visual order. |
| Enter | Follows the link, on a card that has an href | The root is a real anchor, so this is browser behaviour. |
| Space | Scrolls the page | A link activates on Enter, not Space. |
| Pair | Theme | APCA Lc | WCAG 2.2 | Floor |
|---|---|---|---|---|
| body text on the page | light | 101.6 | 17.18:1 | Pass |
| body text on the page | dark | -100.5 | 18.00:1 | Pass |
| body text on a card | light | 104.7 | 17.96:1 | Pass |
| body text on a card | dark | -99.6 | 16.32:1 | Pass |
| body text on the muted ground | light | 98.1 | 16.32:1 | Pass |
| body text on the muted ground | dark | -97.5 | 13.77:1 | Pass |
| secondary text on the page | light | 83.9 | 7.42:1 | Pass |
| secondary text on the page | dark | -80.7 | 13.44:1 | Pass |
| secondary text on a card | light | 87.0 | 7.76:1 | Pass |
| secondary text on a card | dark | -79.8 | 12.19:1 | Pass |
| secondary text on the muted ground | light | 80.4 | 7.05:1 | Pass |
| secondary text on the muted ground | dark | -77.8 | 10.28:1 | Pass |
| a hairline boundary on the page | light | 19.3 | 1.41:1 | Below floor |
| a hairline boundary on the page | dark | -8.2 | 1.90:1 | Below floor |
| a hairline boundary on a card | light | 22.4 | 1.47:1 | Below floor |
| a hairline boundary on a card | dark | -7.3 | 1.72:1 | Below floor |
| a hairline boundary on the muted ground | light | 15.8 | 1.34:1 | Below floor |
| a hairline boundary on the muted ground | dark | 0.0 | 1.45:1 | Below floor |
| a placeholder boundary on the page | light | 69.8 | 4.52:1 | Pass |
| a placeholder boundary on the page | dark | -51.0 | 7.62:1 | Pass |
| a placeholder boundary on a card | light | 72.8 | 4.72:1 | Pass |
| a placeholder boundary on a card | dark | -50.1 | 6.91:1 | Pass |
| a placeholder boundary on the muted ground | light | 66.2 | 4.29:1 | Pass |
| a placeholder boundary on the muted ground | dark | -48.1 | 5.83:1 | Pass |
| a placeholder fill on the page | light | 8.3 | 1.18:1 | Below floor |
| a placeholder fill on the page | dark | -8.2 | 1.90:1 | Below floor |
| a placeholder fill on a card | light | 11.4 | 1.23:1 | Below floor |
| a placeholder fill on a card | dark | -7.3 | 1.72:1 | Below floor |
| a placeholder fill on the muted ground | light | 0.0 | 1.12:1 | Below floor |
| a placeholder fill on the muted ground | dark | 0.0 | 1.45:1 | Below floor |
| a placeholder boundary on its own fill | light | 58.9 | 3.84:1 | Pass |
| a placeholder boundary on its own fill | dark | -41.5 | 4.01:1 | Below floor |
| the card hairline on the page | light | 69.8 | 4.52:1 | Pass |
| the card hairline on the page | dark | -28.7 | 4.19:1 | Below floor |
| the card hairline on a card | light | 72.8 | 4.72:1 | Pass |
| the card hairline on a card | dark | -27.9 | 3.80:1 | Below floor |
| body text on the canvas material | light | 104.7 | 17.96:1 | Pass |
| body text on the canvas material | dark | -100.5 | 18.00:1 | Pass |
| body text on the inset material | light | 98.1 | 16.32:1 | Pass |
| body text on the inset material | dark | -100.1 | 17.18:1 | Pass |
| body text on the card material | light | 104.7 | 17.96:1 | Pass |
| body text on the card material | dark | -99.6 | 16.32:1 | Pass |
| body text on the raised material | light | 104.7 | 17.96:1 | Pass |
| body text on the raised material | dark | -97.5 | 13.77:1 | Pass |
| body text on the sheet material over the darkest backdrop | light | 99.9 | 16.76:1 | Pass |
| body text on the sheet material over the darkest backdrop | dark | -99.7 | 16.47:1 | Pass |
| body text on the sheet material over the lightest backdrop | light | 104.7 | 17.96:1 | Pass |
| body text on the sheet material over the lightest backdrop | dark | -80.8 | 6.07:1 | Pass |
| body text on the overlay material over the darkest backdrop | light | 85.9 | 13.48:1 | Pass |
| body text on the overlay material over the darkest backdrop | dark | -99.9 | 16.77:1 | Pass |
| body text on the overlay material over the lightest backdrop | light | 104.7 | 17.96:1 | Pass |
| body text on the overlay material over the lightest backdrop | dark | -54.2 | 2.69:1 | Below 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.
Related
- 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.
Surface
The primitive behind every panel, sheet and card. It decides how solid or translucent a layer looks, and guarantees that whatever sits on it stays readable.
Sheet
A panel that slides up from the bottom, stops at sensible heights, and can be dragged or dismissed. The one-handed way to act without leaving the page.