Card
A white box with a grey border and 12px corners that groups related content. It lifts slightly on hover.
import { Card } from "@/ui";Card is a plain <div> with a background, a border, rounded corners and a hover effect. It has no padding, header or footer: you add the layout inside it.
No screen imports Card yet. About 20 places in src/pages write the same rounded-xl border border-[var(--border-primary)] classes by hand. Use Card for new work so the style stays in one place.
Basic card
Give it padding and a width with className. p-4 (16px) is the usual padding.
<Card className="p-4 w-[300px]">
<div className="text-[14px] font-medium">Pipeline by channel</div>
<div className="text-[12px] text-[var(--color-text-secondary)] mt-1">
Paid search $268K, LinkedIn $191K, Events $140K.
</div>
</Card>| Property | Value |
|---|---|
| Background | --bg-tertiary (#FFFFFF) |
| Border | 1px --border-primary (#D4D9EA) |
| Radius | 12px (rounded-xl) |
| Padding | None |
| Hover | Shadow --shadow-card (0 8px 24px, 15% grey) and a 2px lift |
| Transition | All properties, 200ms |
Card uses the older --bg-tertiary and --border-primary variables rather than --color-white and --color-grey-200. The values are the same today.
A row of metrics
Cards line up well as a row of KPIs. Hover over one to see the lift.
<Card className="p-4 w-[200px]">
<div className="text-[12px] text-[var(--color-text-secondary)]">Sourced pipeline</div>
<div className="text-[14px] font-semibold">$1.2M</div>
<div className="text-[12px] text-[var(--color-green)]">+18% vs last quarter</div>
</Card>A card with an action
Card is not a button or a link. When the card leads somewhere, put a real Button or link inside it, so it can be reached with the keyboard.
<Card className="p-4 w-[300px] flex flex-col gap-3">
<div className="flex items-center justify-between gap-2">
<span className="text-[14px] font-medium">Q3 webinar series</span>
<Tag color="success-green">Live</Tag>
</div>
<div className="text-[12px] text-[var(--color-text-secondary)]">
$412K pipeline from 96 leads. Paid search and LinkedIn.
</div>
<Button variant="blueGhost" size="sm" label="View campaign" icon={ArrowRight} iconPosition="suffix" onClick={openCampaign} />
</Card>Turning off the hover
The hover lift is always on, which suggests the card can be clicked. For a card that does nothing on click, override it in className. Hover over the card below: it stays still.
<Card className="p-4 w-[200px] hover:shadow-none! hover:translate-y-0">
…
</Card>There is no prop for this. tailwind-merge replaces the lift (hover:translate-y-0 wins over hover:-translate-y-[2px]), but it does not see hover:shadow-none as clashing with hover:shadow-[var(--shadow-card)], so both stay and the card's shadow wins. The ! makes the override important. A static card is the more common case, so the hover default works against most uses.
Props
| Prop | Type | Default | Notes |
|---|---|---|---|
children | ReactNode | ||
className | string | Merged with cn (clsx and tailwind-merge), so it can override padding, radius and hover. |
Any other prop (onClick, role, aria-*, data-*) is passed to the <div>.
Accessibility
- Card has no role. Screen readers read its contents in order, which is what you want for grouped content.
- Do not put
onClickon a Card on its own: a<div>cannot be focused or used with the keyboard. Put a Button or link inside instead. - When a list of cards is the main content of a page, give each card a heading so people can jump between them.
Do and don't
- Do use Card to group one object or one metric: a campaign, an integration, a KPI.
- Do keep padding consistent within a row of cards.
- Don't nest cards inside cards. Use spacing or a divider inside one card.
- Don't change the border colour or radius per screen. If a design needs a different card, that needs a design decision first.
- Don't leave the hover lift on cards that do nothing when clicked.