A surface container with configurable padding and an optional header (icon + title + an `action` slot).
A surface container with configurable padding and an optional header (icon + title + an action slot).
Usage
1import { Card } from "@behivetech/atoms.card";23function Example() {4 const onClose = () => alert("closed");5 return (6 <>7 <Card padding="md">Content</Card>89 <Card title="Settings" padding="md">10 Content11 </Card>1213 <Card14 icon="π§©"15 title="Block"16 action={<button onClick={onClose}>β</button>}17 padding="md"18 >19 Content20 </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 Content3</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/icons.all";34function 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}
Examples
Open all 8 β1const DefaultCard = () => (2 <Card padding="md">Default card with md padding</Card>3);45render(<DefaultCard />);
1const NoPaddingCard = () => (2 <Card padding="none">Card with no padding</Card>3);45render(<NoPaddingCard />);
Properties
Card props
| Prop | Type | Default | Description |
|---|---|---|---|
action | ReactNode | β | Optional content rendered at the end of the header (buttons, menus, a close control, etc.) |
children | ReactNode | β | Card body content |
className | string | β | Additional class names to merge with the component root element |
headingLevel | 1 | 2 | 3 | 4 | 5 | 6 | 3 | Which 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. |
icon | ReactNode | β | Optional icon rendered before the title in the header |
onSelect | MouseEventHandler<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" | none | Controls internal padding; defaults to none so consumers opt in. Applies to the body when a header is present. |
pressed | boolean | β | 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. |
scrollableBody | boolean | false | Keeps 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). |
title | ReactNode | β | 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, β¦).