Stats Cards
betaA row of stat cards, each with a label, a value, and an optional change badge, with loading and empty states.
Preview
- Active users12,480since last week
- Revenue$48.2ksince last month
- Errors37in the last hour
- Uptime99.98%this quarter
loading
Loading stats
empty
Stats show up after your first sale.
narrow container
- Active users12,480since last week
- Revenue$48.2ksince last month
Install
1. Add the registry to
components.json, once per app. Without it, the shadcn CLI looks up@txbrendanin a public list that txbrendan/ui does not control.npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.
npx shadcn@latest add @txbrendan/theme3. Add the block. It also adds every component it uses. Its own files go in your
componentsfolder, and they are yours to edit.npx shadcn@latest add @txbrendan/stats-cards
Use it for
- The top of a dashboard: two to four numbers people check first.
- Any place that shows a few key numbers side by side, like a report summary or an account page.
- Format each value and change yourself, so numbers, money, and dates read the way your app writes them.
- Pick each change's tone from what it means. Fewer errors is good news, so a drop there is success.
Avoid it for
- More than eight numbers. Use a table, so people can sort and compare.
- Numbers over time. Use a chart, and put the latest value in a stat card beside it.
- A single number in running text. Write it in the sentence.
Props
- items
One card per item, in order. Labels must be unique, since each one is a key.
{ label: string; value: string; change?: string; tone?: "neutral" | "brand" | "danger" | "success" | "warning"; description?: string }[]
- loading
Shows placeholder cards: one per item, or four when there are no items yet.
boolean · default false
- loadingText
What the status line tells screen readers while loading.
string · default "Loading stats"
- emptyText
Shown when there are no items and nothing is loading.
string · default "No stats yet."
- className
Extra classes on the outer div, which holds the container query.
string
| Prop | Type | Default | What it does |
|---|---|---|---|
| items | { label: string; value: string; change?: string; tone?: "neutral" | "brand" | "danger" | "success" | "warning"; description?: string }[] | - | One card per item, in order. Labels must be unique, since each one is a key. |
| loading | boolean | false | Shows placeholder cards: one per item, or four when there are no items yet. |
| loadingText | string | "Loading stats" | What the status line tells screen readers while loading. |
| emptyText | string | "No stats yet." | Shown when there are no items and nothing is loading. |
| className | string | - | Extra classes on the outer div, which holds the container query. |
Accessibility
- The cards are a list, so screen readers say how many stats there are. Each card reads its label first, then its value.
- The change is text in a badge. Its color adds meaning but never carries it alone.
- While loading, the list has aria-busy="true" and a status line says loadingText. That line stays on the page, outside the list, so screen readers hear it.
- The cards have no headings. If people should jump between them, put the block under a heading of your own.
- Nothing in the block takes focus. Put links and buttons near it, not inside a card.
Examples
Four stats
<StatsCards
items={[
{ label: "Active users", value: "12,480", change: "+12%", tone: "success", description: "since last week" },
{ label: "Revenue", value: "$48.2k", change: "+4%", tone: "success", description: "since last month" },
{ label: "Errors", value: "37", change: "+9", tone: "danger", description: "in the last hour" },
{ label: "Uptime", value: "99.98%", description: "this quarter" },
]}
/>While the numbers load
<StatsCards items={stats ?? []} loading={stats === undefined} loadingText="Loading account stats" />With nothing to show yet
<StatsCards items={[]} emptyText="Stats show up after your first sale." />