Components/Layout
Card
Cards are self-contained surfaces that each carry one unit of information. They group a piece of content with whatever the reader needs in order to act on it, so a page can set many of them side by side without the boundaries between them blurring. The name at the top and the action at the bottom are what tell a reader whether this is the one to deal with.
import { Card } from '@growth-protocol-ai/alpha';A third APAC disruption this month, and two known suppliers sit in it — one of them single-source.
When to use
Reach for a card when a screen has several things to report and the reader will act on one of them at a time. Give each card one thing to say, one figure to say it with and one action to take about it — a card that carries two unrelated readings is two cards, and the reader has to unpick which of them the action belongs to.
When not to use
Do not use a card where the arrangement is the point rather than the unit: rows of blocks that each carry their own toolbar, or a board that scrolls inside itself, are the Canvas module, and Card is what goes on it. The tell is whether you are reaching for the component to hold content or to place it — placement belongs to the container, and a card never knows where it sits.
variant3 values
What picks a variant is what the card is sitting on, not what it holds. A standalone card is on the page and has to define itself, so it carries the border. A tile is inside something that already draws the outer chrome, so a border of its own would be a second one and the tonal step does the work instead. The two pair their radius with their padding — a large radius with generous padding, a small one with tight — and mixing them is what makes a card look borrowed from another screen.
A third APAC disruption this month, and two known suppliers sit in it.
| Supplier | Region | Spend |
|---|---|---|
| Kestrel Components | APAC | $1.24m |
| Nordwerk GmbH | EMEA | $860k |
| variant | When to use |
|---|---|
standalone | On the page background. --surface-depth-0 over --border-default at --radius-lg, with 20px of padding. The default, and what a metric card or a summary on a detail page is. |
tile | Inside a container that has its own surface — a modal, a panel, a drawer, or another card. --surface-depth-1, no visible border, --radius-md, 16px. The step in tone is deliberately faint; the container is what separates it from the page. |
table | A container rather than a content card: padding drops to zero so row separators run edge to edge, the content clips to the radius, and the radius is --radius-md because a full-bleed table has nothing between its rules and a wider arc. Give it a CardToolbar and a grid, not the content parts. |
status4 values
Status is urgency — what the reader should do — and not health. Today’s critical is next week’s ok, which is the point: a card is a thing to deal with, and things to deal with have urgency rather than a condition. It draws one thing, the dot in the head, and that dot is fixed by the status alone: no variant and no hover state repaints it, and it carries the theme’s status colour as everything else does. Leave status off entirely and no dot renders, which is different from neutral.
A third APAC disruption this month, and two known suppliers sit in it.
A third APAC disruption this month, and two known suppliers sit in it.
A third APAC disruption this month, and two known suppliers sit in it.
A third APAC disruption this month, and two known suppliers sit in it.
| status | When to use |
|---|---|
critical | Act now. Something is demanding attention today. |
warning | Watch. It could escalate, and it has not yet. |
ok | No action required. Not the same as “the thing is perfect”. |
neutral | Informational, no urgency — a card that belongs in a status-bearing group but has nothing to report. Reach for it to keep a row of dots aligned; omit status entirely when the card is not that kind of card at all. |
Three master agreements lapse this quarter. Legal has the redlines.
Every check passed. This is the one to read first.
Borderless at rest, like every other tile in the group.
| State | When to use |
|---|---|
A card that goes somewhere | An href makes the root an anchor: the cursor changes, the border promotes to --button-primary under the pointer, and :focus-visible rings it. At rest it is indistinguishable from a card that goes nowhere, which is deliberate — the difference belongs under the pointer rather than in a group of cards sitting still. |
One card that matters more | highlighted draws the border in the status colour and leaves the fill exactly as its siblings have it, because the border is the whole signal and it works only while no other card in the group has one. On a tile that border is the only chrome there is; on a standalone card it recolours the border already drawn, so pair it with a status or nothing changes. Hover a highlighted card that is also a link and it promotes to the action colour anyway — what you can do outranks what it is. |
Part order is fixed and what you choose is which parts, not where they go: CardHead, then CardValue, then a chart, then CardBody, then CardFooter. Only the head is expected in every card.
There is no chart part, because Alpha ships no charts. The card is a flex column with a gap of its own, so a chart rendered between CardValue and CardBody already lands in the right place at the right spacing.
A chart never replaces the value row. Where the chart is the figure — a gauge three quarters full — the value row still carries it in text. The redundancy is the point: text for precision, the drawing for urgency.
A card with no href gets no cursor and no hover, and that absence is a signal. Do not add one to a card that goes nowhere.
An interactive card is a real anchor, so it answers a middle-click, a right-click and the keyboard, and it takes the focus ring on :focus-visible. Under asChild the element is yours and so are its semantics — a div with a click handler is none of those things.
The status dot is a role="img" whose accessible name is the status, because the colour is the only thing carrying it. Pass statusLabel where “Critical” is not what the reader needs to hear.
Card renders a div, adds no landmark, and puts the name in CardHead in a span rather than a heading — so a screen of cards does not invent a heading outline the page never asked for. Give asChild an <article> where a card is a document in its own right, and put a heading of your own above the group rather than inside each card.
Make the call to action a specific verb — “Review the five insights”, “Approve and start qualification”. “View details” and “Learn more” carry no decision, and the card exists to answer what the next step is.
Leave the call to action subdued. It is --text-typical rather than the brand colour, because on a screen of cards primary-coloured labels compete with the status dots for the same attention — and the dot is what should win. The arrow is what says the card is clickable.
A card sets no width and knows nothing about its position: there is no first-child styling and no sibling-relative rule anywhere in it, so a list of them can be filtered, reordered, paginated or virtualised and nothing visual moves. The arrangement belongs to the container, which reaches its cards with [data-slot="card"] — and the action rows line up across a row only because that container stretches its items to the tallest of them.
Card takes no id. Nothing inside it reads one — pass data-card-id straight through where a renderer or your analytics needs a stable key on the element.
A table card has no content parts. Give it a CardToolbar and a grid; a heading for the table belongs above the card rather than inside it, because the padding is zero and a head would sit flush against the edge.
Which parts, not which order
You choose which parts a card includes and never where they go, because a row of cards that each reordered themselves is a row the reader has to parse one at a time. Only the head is expected in every card. The footer is pinned to the bottom, so across a stretched row every call to action lands on one line however long the bodies above them run — and its glyph is a prop rather than a part, which is what stops a card growing a second call to action.
No figure worth showing — the sentence is the whole of the content.
When the destination is on your own element
href is the short way: the root becomes an anchor, and target and rel are forwarded to it. Where the element is yours — a framework Link, an <article> that owns the card’s semantics — asChild renders that instead, and interactive is how you ask for the cursor and the hover, since there is no href on this component to infer them from.
| Owner | Prop | Type | Default | Notes |
|---|---|---|---|---|
Card | variant | "standalone" | "tile" | "table" | "standalone" | Which chrome the card wears. What picks it is the surface underneath rather than the content. |
Card | status | "critical" | "warning" | "ok" | "neutral" | — | Draws the dot in CardHead and colours it. Omitted is not neutral — it is no dot at all. |
Card | statusLabel | string | the status, capitalised | Accessible name for the dot, which is a role="img" with no text of its own. |
Card | highlighted | boolean | false | Status-coloured border, to pick one card out of a group. |
Card | href | string | — | Renders the root as an <a> and makes the card interactive. A real link, so it takes a middle-click and a right-click like one. |
Card | target | HTMLAttributeAnchorTarget | — | Forwarded to the anchor. Pair _blank with a rel. |
Card | rel | string | — | Forwarded to the anchor. |
Card | interactive | boolean | whether there is an href | Overrides the cursor and hover gate. The reason it exists is asChild, where the href is not on this component. |
Card | asChild | boolean | false | Render the child element instead of the root, keeping the styling — a framework <Link>, or an <article> where the card is a document in its own right. |
Card | children | ReactNode | — | The parts, in the one order they go in. A chart of your own goes between CardValue and CardBody. |
Card | CardHead | part | — | The name row, and the one part expected in every card. It goes first, and the status dot is drawn in it. |
Card | CardValue | part | — | The headline figure. It goes after the head, and it stays even where a chart beside it says the same thing. |
Card | CardBody | part | — | The paragraph that says what to do about the figure. It goes after the value row and after any chart. |
Card | CardFooter | part | — | The action row. It goes last, one per card, and is pinned to the bottom of whatever height the card ends up at. |
Card | CardToolbar | part | — | A table card’s control strip, above the grid and separated from it by a rule. Only the table variant has a place for it. |
CardHead | children | ReactNode | — | The card’s name, in a span of the component’s own. A label rather than a sentence — the sentence is the body’s. |
CardValue | children | ReactNode | — | The figure, as text. The card it sits in decides how big: a tile sets it smaller than a standalone card does, so the same value row reads correctly in both. |
CardValue | delta | ReactNode | — | The small line beside the value: a change, a target, a comparison. It sits on the value’s baseline and wraps under it when the row runs out. |
CardBody | children | ReactNode | — | The paragraph’s content. The block is a real <p>, so a div inside it is markup the browser will unnest. |
CardFooter | children | ReactNode | — | The call to action’s label, beside the glyph in one span. It is not a button — the card is the target. |
CardFooter | icon | ReactNode | <ArrowRightIcon /> | The trailing glyph. Pass null for none, or <ArrowUpRightIcon /> where the action leaves the product. |
CardToolbar | children | ReactNode | — | Filters and actions for the grid below, laid out in a wrapping row with a gap of the strip’s own. |