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.
Summary
A frame around content it does not own.
| APG pattern | none |
| Built on | native elements |
| Status | stable |
| Since | 0.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.
| Part | Element | Required |
|---|---|---|
action | the caller's | the caller's |
body | the caller's | the caller's |
footer | the caller's | the caller's |
media | the caller's | the 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
| Property | Values | Default | What it does |
|---|---|---|---|
interactive | boolean | none | The 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. |
as | keyof React.JSX.IntrinsicElements | none | The 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. |
children | React.ReactNode | none | The 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
| State | Expressed 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.mjstest/browser/site.spec.mjs
Known gaps
_No upstream requirement is recorded as not applicable._
Appearance
| Token | Used for |
|---|---|
--se-border | border, border-block-start |
--se-border-width | border, border-block-start |
--se-duration-instant | transition |
--se-ease-standard | transition |
--se-focus-color | outline |
--se-focus-offset | outline-offset |
--se-focus-width | outline |
--se-radius-lg | border-radius |
--se-space-2 | gap |
--se-space-3 | padding |
--se-space-4 | padding |
--se-surface | background |
--se-surface-sunk | background |
--se-text | color |
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.