Card
Quiet bordered card surface for discrete, self-contained content, including cards with actions composed into footer slots.
Status: stable. Group: structure. Markdown version: /design-system/components/card.md
When to use
Card is the quiet surface for discrete, self-contained content; things that could be reordered, removed, or interacted with independently. The container owns background, hairline border, radius, and padding; variant only swaps the background (default with hairline, muted, transparent) and padding picks a step on the space scale. A thin content layer covers the common patterns: title (a real heading at xxs), description, headerStart and headerEnd header regions (headerEnd replaces the deprecated extra) and footerStart/footerEnd action regions. Everything richer; dividers, footers, media, tabs; is composition with Divider, FlexBox, Stack, and friends. link turns the whole card into exactly one anchor with the focus ring on the frame.
When not to use
- Do not use the muted variant to encode status; status belongs to AlertBanner or Badge.
Look-alikes
- AlertBanner: Do not use the muted variant to encode status; status belongs to AlertBanner or Badge.
- Badge: Do not use the muted variant to encode status; status belongs to AlertBanner or Badge.
- LocationCard: Do not use as a generic 'card with an icon and label'; that is
Card. - Section: Do not wrap page sections or whole forms in cards, and never nest cards in cards; that is Section/Stack territory.
- SelectableCard: Do not nest buttons or links inside a selectable card; use Card for action-oriented content.
- Stack: Do not wrap page sections or whole forms in cards, and never nest cards in cards; that is Section/Stack territory.
- ValueCard: Do not use for content with a primary CTA; that is
ServiceCardorCard.
Props
| Prop | Type | Default | Description | Required |
|---|---|---|---|---|
variant | "default" | "muted" | "transparent" | default | Background variant. All variants keep a transparent border so switching never causes layout jitter. - default: surface background with a hairline border - muted: recessed light background, no visible border - transparent: no background, no border; grouping without weight. | No |
padding | "sm" | "md" | "lg" | "none" | md | Internal padding step on the space scale (12/16/24px) | No |
as | "article" | "div" | "section" | "aside" | "li" | div | Semantic element for the card surface. | No |
title | string | none | Optional heading rendered at the top of the card. | No |
titleProps | CardTitleProps | {} | Heading configuration. | No |
description | string | none | Optional supporting line under the title. | No |
descriptionProps | CardDescriptionProps | {} | Description configuration. | No |
headerStart | React.ReactNode | none | Leading header region. Defaults to the title heading block when omitted. | No |
headerEnd | React.ReactNode | none | Trailing header region (badge, metadata, menu). Canonical replacement for extra. | No |
extra | React.ReactNode | none | No description in the contract. Deprecated: Deprecated. | No |
footerStart | React.ReactNode | none | Leading footer region; the special/destructive action, pinned left. | No |
footerEnd | React.ReactNode | none | Trailing footer region; 1–2 action buttons, pinned right. | No |
link | string | none | Makes the whole card one link to this href. | No |
linkLabel | string | none | Accessible name for the link when title alone is ambiguous. | No |
loading | boolean | false | Skeleton loading state: the card is aria-busy and its skeleton is the one status region ("Loading"). | No |
children | React.ReactNode | none | No description in the contract. | No |
Variants
- Available variants:
default,muted,transparent(defaultdefault). - Available paddings:
none,sm,md,lg(defaultmd). - Allowed
asvalues:article,div,section,aside,li(defaultdiv).
Invalid combinations
- MUST NOT: When
linkis set, then do not setfooterStartorfooterEnd.linkis ignored whenfooterStart/footerEndare set: a stretched anchor cannot wrap interactive footer controls. Use a plain card with buttons instead.
Keyboard
| Key | Behaviour |
|---|---|
| Tab | Moves focus to the control and on to the next focusable element. (standard) |
| Enter | Activates the focused control. (standard) |
Rows marked (standard) name a key the contract lists without a description; the behaviour shown is the usual one for that key.
Rules
Every item under When not to use is a MUST NOT. MUST and SHOULD follow RFC 2119.
| Level | Rule | How it is checked |
|---|---|---|
| MUST NOT | When link is set, then do not set footerStart or footerEnd. link is ignored when footerStart/footerEnd are set: a stretched anchor cannot wrap interactive footer controls. Use a plain card with buttons instead. | Checked by the MCP tool validate_component_usage (rule card-link-with-footer). Also enforced in code: Card.tsx development warning (linkSuppressed). |
| MUST | ARIA: Title renders a real heading (level via titleProps.level, default 3) | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | ARIA: Link mode renders exactly one stretched anchor named by linkLabel ?? title; focus ring draws around the card frame. | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | ARIA: Loading sets aria-busy on the card and renders skeleton lines; the skeleton is the one status region and announces the translated "Loading". | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | ARIA: Plain cards are not focusable and add no roles. | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | The accessibility tree (role, name, state) matches the committed snapshot for every required story. | Automated by npm run test:stories:matrix:ci. WCAG 1.3.1, 4.1.2. |
| MUST | The component remains legible and operable under a real browser forced-colors: active pass. | Automated by npm run test:stories:hc. |
| MUST | Every documented keyboard interaction works as specified. | Not yet verified: no evidence record. WCAG 2.1.1, 2.1.2. |
| MUST | A human completed an end-to-end accessibility review of this component. | Manual review, last on 2026-11-04. |
| SHOULD | Compose structure: Title/Text children, a Divider where the split carries meaning, a FlexBox row as the footer; the card is just the surface. | Not stated in the contract. |
| SHOULD | Keep padding consistent across sibling cards in a grid; pick one step per surface. | Not stated in the contract. |
| SHOULD | Set titleProps.level to keep the document outline honest; grids under a section heading typically use level 3. | Not stated in the contract. |
| SHOULD | Pass linkLabel on linked cards whose title is ambiguous out of context; "Read more" is the failure mode. | Not stated in the contract. |
| SHOULD | Use muted for de-emphasised siblings and transparent to group without visual weight; reach for spacing and headings before adding cards at all. | Not stated in the contract. |
| SHOULD NOT | Do not nest interactive controls inside a link-mode card; use a plain card with a single Button instead. | Not stated in the contract. |
| SHOULD NOT | Do not wrap page sections or whole forms in cards, and never nest cards in cards; that is Section/Stack territory. | Not stated in the contract. |
| SHOULD | Put the destructive/special action in footerStart and the confirm/cancel buttons in footerEnd; footerEnd hugs the right even when footerStart is empty. | Not stated in the contract. |
| SHOULD | When you replace the title with custom headerStart content, keep a real heading inside it so the document outline stays honest. | Not stated in the contract. |
| SHOULD NOT | Do not combine link mode with the action footer: a stretched anchor cannot wrap buttons, so Card drops the link and warns in dev when footerStart/footerEnd are set. Likewise keep interactive content (a menu) out of a linked card's header; Card cannot detect a ReactNode's interactivity there, so that case is the consumer's responsibility. | Not stated in the contract. |
Install and import
- Install:
npm install @digitaltableteur/react(version 0.9.1, restricted access on npm). - Import:
import { Card } from "@digitaltableteur/react"; - Entry point:
@digitaltableteur/react/layoutexports it too. - Styles: import
@digitaltableteur/react/style.cssonce at the app boundary. - Also available as the custom element
<dt-card>; see the custom elements manifest.
Links
- Machine-readable contract (Design System Contract 1.0)
- Markdown version of this page
- Component index as JSON
- All components
- Source: The implementation lives in a private repository. This page, its Markdown twin and the contract are generated from it at build time and are the public source of truth.