NimUI
ComponentsLayout

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

Basic Card

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.

Full Card 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

Card Without Footer

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.

Hoverable Card

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-*.

Card as a Navigation Target

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.

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.

Custom Styled Cards

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

NameTypeDefaultDescription
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.
hoverablebooleanfalseHover 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.
classNamestring-Additional CSS classes for custom styling (width, shadow, border, etc.)
children*ReactNode-Card content, typically CardHeader, CardContent, and/or CardFooter

CardHeader

NameTypeDefaultDescription
classNamestring-Additional CSS classes to apply to the header area
children*ReactNode-Header content (title, description, etc.)

CardContent

NameTypeDefaultDescription
classNamestring-Additional CSS classes to apply to the content area
children*ReactNode-Main card content

CardFooter

NameTypeDefaultDescription
classNamestring-Additional CSS classes to apply to the footer area
children*ReactNode-Footer content (actions, links, metadata)

Accepts every <a> attribute. href is required in practice: without one the element has no link role and no keyboard reachability.

NameTypeDefaultDescription
href*string-Destination. An anchor without href is not a link and is not focusable.
classNamestring-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.) in CardHeader for proper document structure, and put CardLink inside 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-10 on itself, or the link's overlay swallows its click. Do not put it on CardHeader/CardFooter — that lifts the whole box and breaks the card-wide hit area
  • Do not add role="button" and tabIndex={0} to the card. That invents a control with no accessible name, no Space-key behaviour, and none of the browser's link handling — CardLink is 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.

  • Grid - Arrange cards in responsive grid layouts
  • Stack - Stack cards vertically with spacing
  • Flex - Flex layout for card footer actions
  • Modal - Overlay dialog for detailed card interactions

On this page