Skip to content

Stat

A key number with its label and context: a value, an optional change over time and an optional small trend graphic. Unframed: place it in a Card or a grid.

Datav0.1.0experimentalWCAG 2.2 AAView spec
Monthly revenue: $48,200.
import { Stat } from "@rdloom/react";

export default function StatBasicExample() {
  return <Stat label="Monthly revenue" value={48200} format={(n) => `$${n.toLocaleString("en-US")}`} />;
}

Installation

npx rdloom add stat

Copies the source into src/components/rdloom. Edit it freely: rdloom upgrade merges later versions into your changes.

Use another registry client
npx shadcn@latest add https://rdloom.vimalbhatt.com/r/stat.json

Works, but without upgrade tracking.

Usage

import { Stat } from "@rdloom/react";

<Stat label="Monthly revenue" value={48200} format={(n) => `$${n.toLocaleString("en-US")}`} />

With trend

Active users: 12,840. Up 12% compared with last month.
Churn: 2.4%. Up 0.6% compared with last month, worse.
import { Stat } from "@rdloom/react";

export default function StatWithTrendExample() {
  return (
    <div className="flex flex-wrap gap-10">
      <Stat label="Active users" value={12840} trend={{ change: 12, label: "vs last month" }} />
      <Stat label="Churn" value={2.4} unit="%" trend={{ change: 0.6, label: "vs last month", goodWhen: "down" }} />
    </div>
  );
}

With sparkline

Signups: 1,284. Up 8% compared with last week. Across all plans
import { Stat } from "@rdloom/react";

export default function StatWithSparklineExample() {
  return (
    <div className="w-72">
      <Stat
        label="Signups"
        value={1284}
        trend={{ change: 8, label: "vs last week" }}
        data={[12, 18, 14, 22, 19, 27, 31, 28, 36]}
        description="Across all plans"
      />
    </div>
  );
}

Card

Total revenue: $1,250.00. Up 12.5%. Trending up this month. Visitors for the last 6 months
import { Stat } from "@rdloom/react";

// The card look: the trend as a pill at the top right, the number big, a headline, then a note.
export default function StatCardExample() {
  return (
    <div className="w-72 max-w-full">
      <Stat
        variant="card"
        label="Total revenue"
        value="$1,250.00"
        trend={{ change: 12.5 }}
        summary="Trending up this month"
        description="Visitors for the last 6 months"
      />
    </div>
  );
}

Cards row

Total revenue: $1,250.00. Up 12.5%. Trending up this month. Visitors for the last 6 months
New customers: 1,234. Down 20%. Down 20% this period. Acquisition needs attention
Active accounts: 45,678. Up 12.5%. Strong user retention. Engagement exceeds targets
Growth rate: 4.5%. Up 4.5%. Steady performance increase. Meets growth projections
import { Stat } from "@rdloom/react";

// A row of cards. The columns follow the space they have: four across, then two, then one.
export default function StatCardsRowExample() {
  return (
    <div className="grid w-[56rem] max-w-full grid-cols-[repeat(auto-fit,minmax(min(14rem,100%),1fr))] gap-4">
      <Stat variant="card" label="Total revenue" value="$1,250.00" trend={{ change: 12.5 }} summary="Trending up this month" description="Visitors for the last 6 months" />
      <Stat variant="card" label="New customers" value={1234} trend={{ change: -20 }} summary="Down 20% this period" description="Acquisition needs attention" />
      <Stat variant="card" label="Active accounts" value={45678} trend={{ change: 12.5 }} summary="Strong user retention" description="Engagement exceeds targets" />
      <Stat variant="card" label="Growth rate" value="4.5%" trend={{ change: 4.5 }} summary="Steady performance increase" description="Meets growth projections" />
    </div>
  );
}

Loading

Loading Monthly revenue
import { Stat } from "@rdloom/react";

export default function StatLoadingExample() {
  return <Stat label="Monthly revenue" value={0} isLoading />;
}

Row

Revenue: $48,200. Up 12% compared with last month.
Orders: 1,320. Down 3% compared with last month.
Refund rate: 1.8%. No change.
import { Stat } from "@rdloom/react";

export default function StatRowExample() {
  return (
    <div className="grid grid-cols-1 gap-6 sm:grid-cols-3">
      <Stat label="Revenue" value={48200} format={(n) => `$${n.toLocaleString("en-US")}`} trend={{ change: 12, label: "vs last month" }} />
      <Stat label="Orders" value={1320} trend={{ change: -3, label: "vs last month" }} />
      <Stat label="Refund rate" value={1.8} unit="%" trend={{ change: 0 }} />
    </div>
  );
}

API Reference

Defined by the spec. Components also accept the props of the React Aria component they wrap.

PropTypeDefault
labelrequired

What the number measures, e.g. "Monthly revenue".

stringnone
valuerequired

The number. A number is formatted as en-US (the same on the server and in the browser) unless format is given; a string is shown as is.

string | numbernone
format

Formats a numeric value, e.g. as currency.

(value: number) => stringnone
unit

A small unit after the value, such as % or /mo.

stringnone
trend

The change over time. change is signed; label says what it is compared with ("vs last month"); goodWhen says which direction is an improvement (default up).

{ change: number; label?: string; goodWhen?: "up" | "down" }none
data

Recent values, drawn as a small Sparkline beside the value.

number[]none
description

A short line of extra context under the value.

stringnone
summary

A short headline under the number in the card look, e.g. "Trending up this month". It also gets a small trend icon.

stringnone
variant

plain has no frame. card is a soft rounded surface with the trend as a pill at the top right, the number big, then the summary and a note.

"plain" | "card""plain"
size

Size of the value text.

"sm" | "md" | "lg""md"
isLoading

Shows placeholder blocks and announces loading.

booleanfalse

Accessibility

Role text, WCAG 2.2 AA. Tested with axe and keyboard tests; screen reader checks are in the audit checklist.

Keyboard

Not interactive.

Screen readers announce

  • Read in place as one sentence: label, value, change and description
  • Loading reads "Loading" followed by the label

What your code must do

  • The whole stat reads as one sentence, e.g. "Monthly revenue: $48,200. Up 12% compared with last month."
  • Direction is always said in words (Up, Down, No change) and shown with an arrow shape, never by color alone
  • Good or bad is only spoken when goodWhen is set explicitly; the tint is an extra cue
  • Pill text uses the default text color on a subtle tint, meeting 4.5:1 in light and dark mode
  • While loading the region is marked busy and says Loading
  • Not interactive: it is not focusable

Guidelines

Use it when

  • Dashboards and summaries that lead with a few key numbers
  • Showing a number with how it changed

Avoid it when

  • Many numbers that need comparing: use a Table
  • A value that changes live and must be announced: use a live region

Don't

  • Showing a change as only a green or red color
  • Long sentences as the label
  • Putting more than a handful of stats in one row

Design tokens

The semantic tokens this component uses. Change them once and every component follows; see Design tokens.

  • --rd-color-text-default
  • --rd-color-text-muted
  • --rd-color-feedback-success-subtle
  • --rd-color-feedback-danger-subtle
  • --rd-color-surface-subtle
  • --rd-color-border-default