Agent docs

Card

The kit's one surface for grouped content.

The kit's one surface for grouped content: page sections, list pages, form blocks, and stat rows. Purely presentational, it owns spacing, border, and header layout and never holds data state itself.

The canon behind it. docs/design-language/05-cards-and-surfaces.md §A1 (05-cards-and-surfaces.md:15, the universal content-section wrapper, header doubles as documentation), §A2 (05-cards-and-surfaces.md:18, a card-scoped action goes through toolbar, never a sibling div), §A3 (05-cards-and-surfaces.md:21, an already-framed surface uses dividers, not nested cards), and §A4 (05-cards-and-surfaces.md:24, exactly one owner of horizontal inset per framed container).

Toolbar actions. The right-hand button in a card toolbar is never the kit's zero (bare outline plus neutral, no icon, no shade, no tone); it declares itself with an arrow when it navigates and a shade plus a tone when it acts, and it lives in the toolbar slot rather than in a row under the title (canon doc 2 §A10, doc 60 §6, button.md Banned).

When to reach for it#

Any non-table content block on a page wraps in a Card (canon doc 5 §A1: "the universal content-section wrapper"). It is also the list-page shell: a flex-column Card that owns internal scroll while a DataTable fills its body.

Do not reach for it inside an already-framed surface. Peek panels and modals compose their sections with a divider plus a heading, never a nested Card (canon doc 5 §A3: "nesting cards inside a framed overlay is a defect"). Do not reach for it to show a single entity's numeric summary either, that is StatTile, not a Card full of numbers (canon doc 5 §A6).

Contract#

Header (renders only when at least one of these is given):

  • title / subtitle: string or JSX via renderNode. The header is documentation as much as framing, canon doc 5 §A1 calls the pairing out explicitly ("Outputs" / "What this run wrote, part by part").
  • Icon (with iconSize, iconProps, iconClassName, the standard icon prop group): the card's mark, left of the title in the same tinted disc PageTitle wears, one size down (size-8, 16px; size-7, 14px under compact). Standard header meta since the round-6 docs home (owner, 2026-08-19, doc 69 D4): a section card carries the icon of the page it leads to, and the sidebar group for that section carries the same one (NavGroup.Icon, app-shell.md), so the index, the column and the page read as one set. The skeleton keeps the disc's space. iconVariant="disc" (default) wears the disc at rest and darkens one rung under the pointer; iconVariant="plain" is bare at rest and gains the disc when the card is hovered, with a transition. iconTone (primary, secondary, accent, info, success, warning, error) colours icon and disc. Every one of those washes is a rung of the tint ladder (canon doc 01 §A8: faint /5, soft /10, strong /15), chosen once in card.tsx's ICON_DISC map; a page that wants a different shade files feedback against the ladder rather than typing a fresh opacity into iconWrapClassName (owner, doc 70 E2).
  • toolbar: a free JSX slot, right-aligned. The one place a card-scoped action belongs (canon doc 5 §A2).
  • titleDivider: an opt-in rule under the header extending to the card's edges, for list-style cards.

Body:

  • children.
  • compact: tighter padding, reserved for denser config/admin screens, not customer-facing content (canon doc 5 §A7).
  • noBorder: drops the border, for flush or nested placements.
  • shadow: one of the shared SurfaceShadow ladder (none | xs | sm | md | lg), default sm.

State:

  • skeleton: renders a card-shaped placeholder that keeps the real card's border, shadow, and body padding, and deliberately does not animate in ("a placeholder should be there, not arrive"). This is the one sanctioned "content is a skeleton, not a spinner" instance for a whole card (canon doc 4 §A1, §A2).
  • noAnimate: opts out of the entrance animation that is otherwise on by default.
  • stagger: cascades the card's own contents in one after another instead of fading the whole card as one block. Present on the type and implemented in source, but has no confirmed shipped call site in either app as of this pass; SkeletonGroup's own, separate stagger prop is the one actually used everywhere. Do not confuse the two: Card's stagger is for a populated card's real content, SkeletonGroup's is for a group of loading placeholders.

Slots and DOM:

  • Slot classnames per CardClassNames: className, titleClassName, subtitleClassName, toolbarClassName, bodyClassName, iconWrapClassName.
  • containerProps: raw DOM attributes for the card root, className excluded (use className above instead).
  • The body publishes its own horizontal inset as the --card-inset CSS variable (1.5rem, or 1rem under compact), so a full-bleed child (<Table bleed>) can cancel exactly that padding instead of hardcoding a number. See canon doc 5 §A4, "exactly one owner of horizontal inset per framed container."

Sanctioned combinations#

CombinationProducesWhere usedWhy
toolbar={<Chip/> <Chip color="error"/> <Button href .../>}A card header mixing status chips with one actionspeedway/app/routes/admin/team.tsx:373-390 (plan Chip, conditional Suspended Chip, "Open usage console" Button)Canon doc 5 §A2: "Toolbars may mix status chips with one action"
className="mt-4 flex max-h-full min-h-0 flex-col" bodyClassName="flex min-h-0 flex-1 flex-col"The list-page card: grows to the viewport, table scrolls inside itspeedway/app/components/JobsTable.tsx:834-841Canon doc 5 §A5 and §B "List page" preset, the dominant list-page shape across both apps
titleDivider on a card whose body is a stat grid or activity feedA rule separating header from list-style content, extending to the card edgeswalmart-mvp/frontend/src/pages/Admin.tsx:288, 319, walmart-mvp/frontend/src/pages/Home.tsx:93, 115, 118The most common walmart pairing; every list-shaped card in Admin and Home carries it
shadow="sm" stated explicitly even though it is the defaultNo visual change, but future-proofs the call site if the default ever movesEvery <Card> in walmart-mvp (Admin.tsx, Jobs.tsx:909, Login.tsx:102, Settings.tsx:79, and others)A real, consistent divergence from speedway, which never restates the default. Match whichever app you are extending
skeleton standalone, no other content propsA loading placeholder in place of an entire not-yet-loaded cardspeedway/app/routes/account/accounts.tsx:82The box-preserving loading pattern from canon doc 4 §B
noBorder={someCondition}A card whose border is conditional on a caller-owned density flagspeedway/app/components/NewJobForm.tsx:543, 741Shows noBorder composing with a boolean the caller derives, not just a static toggle

Banned combinations#

  • A nested Card inside a Modal or peek panel. Canon doc 5 §A3 names this a defect outright: use a border-t divider plus a heading instead. This has already been caught and corrected once (the doc cites it as a measured violation, doc 10 §A10).
  • Doubling the horizontal inset. A full-bleed child that re-adds its own edge padding instead of canceling the card's (-mx-6 plus first/last-cell padding) overflows by exactly the doubled amount; canon doc 5 §A4 records this happening once ("overflowed 2px") and states the fix as structural: name the single element that owns the inset.
  • compact on customer-facing content cards. Reserved for denser admin and config screens (canon doc 5 §A7); a customer content card that reaches for compact for visual tightness rather than density need is working against the convention.
  • A sibling <div> holding a card-scoped mutating action instead of toolbar. Canon doc 5 §A2 is explicit that this always goes through the toolbar slot.
  • A hand-typed opacity on the header icon disc (iconWrapClassName="bg-primary/8", text-base-content/55): the disc's washes are ladder rungs picked by iconVariant and iconTone; a shade that looks wrong is feedback against the ladder (canon doc 01 §A8), not a one-off number. A deliberate one-off override is allowed, named as such in the commit.

Before you adopt this#

Five questions to answer before reaching for Card.

  1. Does the shell, a parent layout, or a global provider already render this? Not applicable, Card is page content; no shell already renders it.
  2. Does this app already ship a local implementation of the same thing? Not applicable, no local Card clone is documented in either app.
  3. Does this app's kit pin reach the version this component or prop landed in? Not applicable, no version-landed Card prop is noted in this doc.
  4. Does the component derive its own accessible name and keyboard path, or must the call site supply them? Not applicable, Card is a static frame with no control to name.
  5. Which canon §A rules bind this surface, and which does the composition break? §A2 routes a card action through toolbar; §A3 bars nesting inside an overlay.

Travels with#

  • Skeleton internally, for its own skeleton mode, not a caller concern.
  • DataTable, in the list-page shape: Card owns the scroll container and padding, the table cancels the inset on its edge cells (canon doc 5 §A4).
  • StatTile, as a sibling rather than a nesting: a StatTile row sits inside a Card's body as one of several rows, it does not itself need a wrapping Card per tile.
  • Chip and Button, inside toolbar, per the sanctioned-combinations row above.

Snippet#

<Card
title="Usage"
subtitle="This cycle"
toolbar={
<span className="flex items-center gap-2">
<Chip size="sm" variant="soft">{planId}</Chip>
{suspended && (
<Chip size="sm" variant="soft" color="error">Suspended</Chip>
)}
<Button
size="sm"
variant="text"
shade
color="primary"
Icon="Star"
content="Open usage console"
href={`/admin/teams/${org.id}/usage`}
/>
</span>
}
/>

speedway/app/routes/admin/team.tsx:370-393

@versable-git/ui · reference, canon, and method, read in place