Skip to content
txbrendan/ui

Stats Cards

beta

A row of stat cards, each with a label, a value, and an optional change badge, with loading and empty states.

Built from Card, Badge, and Skeleton.

Preview

  • Active users
    12,480
    +12%since last week
  • Revenue
    $48.2k
    +4%since last month
  • Errors
    37
    +9in the last hour
  • Uptime
    99.98%
    this quarter

loading

Loading stats

empty

Stats show up after your first sale.

narrow container

  • Active users
    12,480
    +12%since last week
  • Revenue
    $48.2k
    +4%since last month

Install

  1. 1. Add the registry to components.json, once per app. Without it, the shadcn CLI looks up @txbrendan in a public list that txbrendan/ui does not control.

    npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"
  2. 2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.

    npx shadcn@latest add @txbrendan/theme
  3. 3. Add the block. It also adds every component it uses. Its own files go in your components folder, 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

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." />