Components / surfaces

Card

media is new to the part vocabulary. It names the region that bleeds to a frame's edge, and nothing else in the system had one.

Rendered from the package · open on its own

Summary

A frame around content it does not own.

APG patternnone
Built onnative elements
Statusstable
Since0.2.0
<Card>
  <img data-part="media" data-se-layout="frame" src={cover} alt="" />
  <div data-part="body">
    <h3 data-se-text="crosshead">Northlight</h3>
    <p data-se-text="body">Twelve images, shot over two winters.</p>
  </div>
</Card>

crosshead is the card title's role — the smallest heading, at the lg step. This example reached for heading before that rung existed, and a 2xl page-section title read oversized on a tile.

Anatomy

An <article> by default, carrying data-se="card". Every part is a direct child and every part is a slot.

PartElementRequired
actionthe caller'sthe caller's
bodythe caller'sthe caller's
footerthe caller'sthe caller's
mediathe caller'sthe caller's

media bleeds to the frame and clips to its radius. body is padded. footer is pushed to the bottom, so cards in a row keep their footers on one line whatever the bodies above them do. action marks the one link an interactive card stretches.

Every rule uses the child combinator. Part 3.3 is open, and a descendant selector would dress a nested component's own footer or body.

Properties

PropertyValuesDefaultWhat it does
interactivebooleannoneThe whole card becomes one click target for the link marked data-part="action". The link stays the only focusable thing and keeps its own accessible name; the card draws the focus ring on its behalf. Without this, a card is inert and any number of controls inside it behave normally.
askeyof React.JSX.IntrinsicElementsnoneThe element to render. article by default, because a card is usually a self-contained composition. Use li inside a list, or div where neither fits.
childrenReact.ReactNodenoneThe card's contents, arranged by the caller. Parts are opt-in: media bleeds to the frame, body is padded, footer is pushed to the bottom.

States

StateExpressed as
Focused:focus-visible
Hover:hover

Hover and focus apply only to an interactive card. An inert one has no states at all.

Behavior

None of its own. An interactive card is a positioning context for the link inside it, and the link does the work.

Accessibility

The stretched link is the whole accessibility story. data-interactive makes the card a positioning context and the link marked data-part="action" covers it with a pseudo-element. The link stays the only tab stop and keeps its own accessible name.

Never wrap a card in an anchor. A reader then hears one link whose name is every word inside, and any button within it becomes interactive content nested in a link, which is invalid.

Never put the click on the frame. A div with a handler is not reachable by keyboard.

The card draws the focus ring via :has(), because a reader cannot see the edges of a stretched link.

Heading level is the caller's. Only the page knows whether a card's heading is an h2 or an h3, which is why there is no title prop.

Keyboard

None of its own. An interactive card's link is a normal tab stop; an inert card takes no keys.

Roles, states and properties

_None written. The elements carry their own._

Focus

The card draws the ring for the link inside it. The stretched link has no visible edges, so a ring on the link itself would appear in the wrong place. The link keeps the focus; the card only shows it.

Screen reader

Not recorded as verified. Stage 4 of the readiness ladder — driving each component with VoiceOver and NVDA — is unstarted across the package, and no component should claim otherwise until docs/journal/ records the session that did it.

Contrast and target size

npm run check:contrast audits every color pair this component uses across all four skins, light and dark, and fails the build on a shortfall. Two waivers are on the record in docs/audit/DEFERRED.md.

It sets no target size of its own; anything interactive inside it brings its own.

Tested

  • test/browser/card.spec.mjs
  • test/browser/site.spec.mjs

Known gaps

_No upstream requirement is recorded as not applicable._

Appearance

TokenUsed for
--se-borderborder, border-block-start
--se-border-widthborder, border-block-start
--se-duration-instanttransition
--se-ease-standardtransition
--se-focus-coloroutline
--se-focus-offsetoutline-offset
--se-focus-widthoutline
--se-radius-lgborder-radius
--se-space-2gap
--se-space-3padding
--se-space-4padding
--se-surfacebackground
--se-surface-sunkbackground
--se-textcolor

No shadow. 3.5 reads elevation as an obligation about dismissal, and a card sits in the flow and dismisses nothing.

Motion

Background color crosses --se-duration-instant on hover, and only on an interactive card. Nothing lifts and nothing scales — a card that grows under the pointer moves every card beside it in a grid.

Content

The words are the caller's. What the frame asks for is that the heading names the thing rather than describing it: Northlight, not A case study about Northlight.

An interactive card's link text is what a reader hears in a list of links, so it has to make sense read alone.

Usage

It is a container, not a schema. A card that named its contents — title, description, image — would have to choose their markup, and it would grow a prop for every layout anyone wanted. Configure the very common, compose the rest.

Where the stretched link stops. The pseudo-element covers the card, so text inside an interactive card cannot be selected with a mouse. If the content is something a reader will copy — an address, a key, a quote — make the card inert and link the heading normally.

When not to use it. A card is not a section. If the content is the page rather than one of many things on it, a heading and a Separator say so better than a border does.

Notes

A schema Card was rejected on three grounds, in the order they bind here: it could not ship as anatomy, so a Vue or Astro consumer would get nothing; it would have to pick a heading tag; and it could not express the stretched link at all.

media is new to the part vocabulary. It names the region that bleeds to a frame's edge, and nothing else in the system had one.