A surface container with configurable padding and an optional header (icon + title + an `action` slot).

pnpm add @behivetech/atoms.card

You are viewing Card v0.6.1, snapshotted 2026-11-10. The latest is v0.6.2. Go to the latest β†’

A surface container with configurable padding and an optional header (icon + title + an action slot).

Usage

1import { Card } from "@behivetech/atoms.card";
2
3function Example() {
4 const onClose = () => alert("closed");
5 return (
6 <>
7 <Card padding="md">Content</Card>
8
9 <Card title="Settings" padding="md">
10 Content
11 </Card>
12
13 <Card
14 icon="🧩"
15 title="Block"
16 action={<button onClick={onClose}>βœ•</button>}
17 padding="md"
18 >
19 Content
20 </Card>
21 </>
22 );
23}

The action slot is freeform β€” pass a close button, a menu, multiple buttons, or nothing. If icon, title, and action are all omitted, no header is rendered and children render directly in the card (unchanged from a plain Card). When a header is present, padding applies to the body instead of the whole card, so the header can span edge-to-edge with its own fixed padding.

Heading level

title renders inside an h1–h6 tag (not a span) so it takes its correct place in the page's heading outline β€” set via headingLevel, which defaults to 3. The default assumes the common case: a page's own h1, sections within it titled h2, and a Card nested inside one of those sections landing at h3. Override it when a card sits at a different depth β€” e.g. headingLevel={2} for a card used directly under a page's h1 with no intervening section:

1<Card title="Settings" headingLevel={2} padding="md">
2 Content
3</Card>

Root element

The root renders as a <section>, labelled by title via aria-labelledby, whenever a title is given β€” an HTML5 <section> should have an accessible heading, and title is exactly that. With no title (a bare surface, or a header made up of just icon/action), the root stays a plain <div>, since an unlabelled <section> isn't a meaningful landmark.

Interactive (clickable) cards

Pass onSelect to turn the body into a real <button> β€” hover/focus affordance plus a centered, stacked content layout β€” for tile/action-card patterns like a component picker grid:

1import { Card } from "@behivetech/atoms.card";
2import { Icon } from "@behivetech/atoms.icon";
3
4function Example() {
5 const item = { name: "Hero banner" };
6 const handleSelect = (selected) => alert(selected.name);
7 return (
8 <Card padding="md" onSelect={() => handleSelect(item)}>
9 <Icon name="box" size="lg" />
10 <span>{item.name}</span>
11 </Card>
12 );
13}

This is the same job MUI's CardActionArea does, folded into Card itself rather than a separate component β€” children must stay to phrasing content (text, spans, icons), since a native <button> can't contain block-level elements like <div>. onSelect is deliberately distinct from onClick: onClick is the plain pass-through to the root element (e.g. to stop click propagation on a whole panel), while onSelect specifically drives the interactive-body behavior described here.

Overriding surface styling

background/border/border-radius read from --card-background/--card-border/--card-radius custom properties (falling back to the design tokens above). A consumer embedding a Card for a different context β€” a docked panel with no elevation, a modal with a larger radius β€” can set these on its own wrapping class instead of fighting CSS-module specificity:

1.my-docked-panel {
2 --card-border: none;
3 --card-radius: 0;
4}

Properties

Card props

PropTypeDefaultDescription
actionReactNodeβ€”Optional content rendered at the end of the header (buttons, menus, a close control, etc.)
childrenReactNodeβ€”Card body content
classNamestringβ€”Additional class names to merge with the component root element
headingLevel1 | 2 | 3 | 4 | 5 | 63Which heading tag (`h1`–`h6`) wraps `title`, so the card's title takes its correct place in the page's heading outline. Defaults to `3`, matching a card nested one level below a page's `h1` title and a `section`'s `h2` β€” override per-instance when a card sits at a different depth.
iconReactNodeβ€”Optional icon rendered before the title in the header
onSelectMouseEventHandler<HTMLButtonElement>β€”Makes the body a real `<button>` β€” hover/focus affordance plus a centered, stacked content layout β€” for tile/action-card patterns (a picker grid, for example) instead of a static surface. Distinct from `onClick` (a plain pass-through to the root element, e.g. for stopping click propagation). Omit for a plain, non-interactive card.
padding"none" | "sm" | "md" | "lg"noneControls internal padding; defaults to none so consumers opt in. Applies to the body when a header is present.
pressedbooleanβ€”For a toggle tile (a multi-select picker): sets `aria-pressed` on the body button. Only meaningful together with `onSelect`; leave undefined for a plain action card, which then has no `aria-pressed` at all.
scrollableBodybooleanfalseKeeps the header fixed and scrolls only the body when content exceeds the card's height. Only meaningful when the card has a bounded height (a modal or drawer panel, for example).
titleReactNodeβ€”Optional header title; omit along with `icon`/`action` to render no header

Also accepts the native attributes of its root element (className, aria-*, event handlers, …).