Card
Versatile content container with header, content, and footer sub-components
The Card component is a composite layout element that groups related content within a bordered, rounded container. It uses a compound component pattern with CardHeader, CardContent, and CardFooter sub-components for structured content areas, plus CardLink when the whole card is a navigation target.
Import
import { Card, CardHeader, CardContent, CardFooter, CardLink } from '@nim-ui/components';Basic Usage
This is a basic card with content only.
Code
<Card>
<CardContent>
<p>This is a basic card with content only.</p>
</CardContent>
</Card>Full Card Structure
Use all three sub-components for a complete card layout with header, content, and footer.
Card Title
Card description goes here
This is the main content area of the card. It can contain any type of content including text, images, forms, or other components.
Code
<Card>
<CardHeader>
<h3 className="text-lg font-semibold">Card Title</h3>
<p className="text-sm text-neutral-500">Card description goes here</p>
</CardHeader>
<CardContent>
<p>This is the main content area of the card.</p>
</CardContent>
<CardFooter>
<Button variant="primary" size="sm">Save</Button>
<Button variant="outline" size="sm">Cancel</Button>
</CardFooter>
</Card>Header and Content Only
Notifications
You have 3 unread notifications.
Code
<Card>
<CardHeader>
<h3 className="text-lg font-semibold">Notifications</h3>
</CardHeader>
<CardContent>
<p>You have 3 unread notifications.</p>
</CardContent>
</Card>Hoverable
Set hoverable to make the card respond to the pointer: it lifts by 2px and deepens its shadow, both interpolated over --duration-fast. Hover the card below to see it.
Users who ask for reduced motion keep the shadow crossfade and lose the lift: the counterpart narrows the transition rather than switching it off, because a change of depth is a visual cue and not a vestibular trigger.
hoverable is a hover response, not a claim that the card is clickable. It deliberately ships no pointer cursor: the card is a plain <div>, so a cursor would promise a target that no keyboard or screen-reader user can reach. Use it on its own to make a live panel feel responsive, and add CardLink below when the card really is a target.
Queue throughput
Updated 4 hours ago
1,284 jobs settled · 3 retrying
Code
<Card hoverable>
<CardHeader>
<h3 className="text-lg font-semibold">Queue throughput</h3>
<p className="text-sm text-neutral-500">Updated 4 hours ago</p>
</CardHeader>
<CardContent>
<p>1,284 jobs settled · 3 retrying</p>
</CardContent>
</Card>Card as a navigation target
CardLink is a real anchor whose generated box covers the whole card. A pointer can click anywhere in the card; the keyboard, the screen reader and the browser's own link handling — middle-click, Cmd/Ctrl-click, the context menu, "copy link address" — all see one ordinary link.
The card is never the control, the link is. So the card is not a tab stop, tab order stays document order, and focusing the link draws the focus indicator around the whole card rather than around the title text alone. Tab into the preview below to see it.
That indicator is an outline rather than the kit's usual shadow ring, in the same steel pair (primary-500 / primary-400). A ring paints its offset band in an opaque colour, which at a whole card's perimeter reads as a white halo in dark mode; an outline's offset gap is transparent, it follows the corner radius on its own, and it appears at once instead of fading in with the card's shadow transition. Override it with outline-* utilities, not ring-*.
Acme Corporation
Updated 4 hours ago
14 open invoices · 3 overdue
Code
<Card hoverable>
<CardHeader>
<h3 className="text-lg font-semibold">
<CardLink href="/customers/acme">Acme Corporation</CardLink>
</h3>
<p className="text-sm text-neutral-500">Updated 4 hours ago</p>
</CardHeader>
<CardContent>
<p>14 open invoices · 3 overdue</p>
</CardContent>
<CardFooter>
<Button variant="outline" size="sm" className="relative z-10">Export invoices</Button>
</CardFooter>
</Card>Every control inside the card needs relative z-10
The link's overlay sits above the card's content, so any element that should stay clickable has to be raised above it — on the element itself:
<CardFooter>
<Button className="relative z-10">Export invoices</Button>
</CardFooter>This is deliberately per-control. Putting relative z-10 on CardHeader or CardFooter would lift the whole box above the overlay, and clicking the empty space in that box would then stop reaching the link — on the header that removes most of the card's hit area.
The overlay is positioned against the nearest positioned ancestor, which Card provides. Anything positioned between Card and CardLink shrinks the clickable area to itself.
Known limitation: text selection
The overlay also swallows drag-selection across the card body, so a reader cannot sweep an order ID out of the card with the mouse. Reach for CardLink when the card is primarily a navigation target; when copying text out of it matters more, put a plain Link on the title and skip CardLink — the rest of the card stays selectable and the link is still keyboard-reachable.
Composing with a router link
cardLinkVariants is exported so a framework link can take the same treatment without a Slot dependency. Spread data-card-link alongside it — that attribute is what the card's focus treatment keys on, and the card-wide focus indicator does not appear without it.
import { cardLinkVariants } from '@nim-ui/components';
import NextLink from 'next/link';
<NextLink href="/customers/acme" data-card-link className={cardLinkVariants()}>
Acme Corporation
</NextLink>Custom Styling
Cards accept a className prop for custom styling including width constraints, shadows, and border customization.
Elevated card with shadow-lg
Card with custom border color
Card with custom background
Code
<Card className="shadow-lg">
<CardContent>Elevated card with shadow-lg</CardContent>
</Card>
<Card className="border-primary-500 border-2">
<CardContent>Card with custom border color</CardContent>
</Card>
<Card className="bg-primary-50 dark:bg-primary-900/20">
<CardContent>Card with custom background</CardContent>
</Card>Props
Card
| Name | Type | Default | Description |
|---|---|---|---|
variant | 'default' | 'outlined' | 'elevated' | 'ghost' | 'default' | Surface treatment: default is bordered with a soft shadow, outlined drops the shadow, elevated deepens it, ghost drops the border and background entirely. |
hoverable | boolean | false | Hover response for the whole card: a 2px lift and a deeper shadow, both transitioned. Suppresses the lift under prefers-reduced-motion. Ships no pointer cursor — add CardLink when the card is really a target. |
className | string | - | Additional CSS classes for custom styling (width, shadow, border, etc.) |
children* | ReactNode | - | Card content, typically CardHeader, CardContent, and/or CardFooter |
CardHeader
| Name | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes to apply to the header area |
children* | ReactNode | - | Header content (title, description, etc.) |
CardContent
| Name | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes to apply to the content area |
children* | ReactNode | - | Main card content |
CardFooter
| Name | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes to apply to the footer area |
children* | ReactNode | - | Footer content (actions, links, metadata) |
CardLink
Accepts every <a> attribute. href is required in practice: without one the element has no link role and no keyboard reachability.
| Name | Type | Default | Description |
|---|---|---|---|
href* | string | - | Destination. An anchor without href is not a link and is not focusable. |
className | string | - | Additional CSS classes, merged with the stretched-overlay base |
children* | ReactNode | - | The accessible name of the whole card — keep it specific, not "View details" |
Usage Examples
User Profile Card
function UserProfileCard({ user }) {
return (
<Card className="max-w-sm">
<CardHeader>
<Flex align="center" gap="md">
<Avatar src={user.avatar} initials={user.initials} />
<div>
<h3 className="font-semibold">{user.name}</h3>
<p className="text-sm text-neutral-500">{user.role}</p>
</div>
</Flex>
</CardHeader>
<CardContent>
<p className="text-sm">{user.bio}</p>
</CardContent>
<CardFooter>
<Button variant="primary" size="sm">Follow</Button>
<Button variant="outline" size="sm">Message</Button>
</CardFooter>
</Card>
);
}Settings Card
function SettingsCard() {
return (
<Card>
<CardHeader>
<h3 className="text-lg font-semibold">Account Settings</h3>
<p className="text-sm text-neutral-500">Manage your account preferences</p>
</CardHeader>
<CardContent>
<Stack spacing="md">
<Input label="Display Name" defaultValue="John Doe" />
<Input label="Email" type="email" defaultValue="john@example.com" />
<Textarea label="Bio" rows={3} />
</Stack>
</CardContent>
<CardFooter className="justify-end">
<Button variant="outline">Cancel</Button>
<Button variant="primary">Save Changes</Button>
</CardFooter>
</Card>
);
}Card Grid
function CardGrid({ items }) {
return (
<Grid cols={3} gap="lg">
{items.map((item) => (
<Card key={item.id}>
<CardHeader>
<h3 className="font-semibold">{item.title}</h3>
</CardHeader>
<CardContent>
<p className="text-sm">{item.description}</p>
</CardContent>
<CardFooter>
<Button variant="ghost" size="sm">Learn more</Button>
</CardFooter>
</Card>
))}
</Grid>
);
}Accessibility
Card renders a plain <div> and claims no role of its own. When the card is a target, the target is a link inside it — not the card.
- Use heading elements (
h2,h3, etc.) inCardHeaderfor proper document structure, and putCardLinkinside the heading so the link text is also the heading text - Give the link a specific accessible name. "Acme Corporation" tells a screen-reader user which card they are on; "View details" repeated down a list does not
- Every interactive element inside the card needs
relative z-10on itself, or the link's overlay swallows its click. Do not put it onCardHeader/CardFooter— that lifts the whole box and breaks the card-wide hit area - Do not add
role="button"andtabIndex={0}to the card. That invents a control with no accessible name, no Space-key behaviour, and none of the browser's link handling —CardLinkis the supported way to make the card a target - For card lists, wrap them in a
<section>with a label
{/* The card is a target: one link owns the role, the name and the whole hit area */}
<Card hoverable>
<CardHeader>
<h3><CardLink href="/customers/acme">Acme Corporation</CardLink></h3>
</CardHeader>
<CardContent>14 open invoices · 3 overdue</CardContent>
<CardFooter>
<Button size="sm" className="relative z-10" onClick={handleExport}>Export invoices</Button>
</CardFooter>
</Card>
{/* Card section with heading */}
<section aria-labelledby="team-heading">
<h2 id="team-heading">Team Members</h2>
<Grid cols={3} gap="md">
<Card>...</Card>
<Card>...</Card>
</Grid>
</section>Keyboard behaviour: Tab reaches the link in document order, Enter follows it, and Tab again reaches the footer control. The focus indicator is drawn around the whole card, so it is obvious which card is focused in a grid of them.