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.
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 statCopies 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.jsonWorks, but without upgrade tracking.
Usage
import { Stat } from "@rdloom/react";
<Stat label="Monthly revenue" value={48200} format={(n) => `$${n.toLocaleString("en-US")}`} />With trend
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
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
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
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
import { Stat } from "@rdloom/react";
export default function StatLoadingExample() {
return <Stat label="Monthly revenue" value={0} isLoading />;
}Row
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.
| Prop | Type | Default |
|---|---|---|
labelrequiredWhat the number measures, e.g. "Monthly revenue". | string | none |
valuerequiredThe 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 | number | none |
formatFormats a numeric value, e.g. as currency. | (value: number) => string | none |
unitA small unit after the value, such as % or /mo. | string | none |
trendThe 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 |
dataRecent values, drawn as a small Sparkline beside the value. | number[] | none |
descriptionA short line of extra context under the value. | string | none |
summaryA short headline under the number in the card look, e.g. "Trending up this month". It also gets a small trend icon. | string | none |
variantplain 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" |
sizeSize of the value text. | "sm" | "md" | "lg" | "md" |
isLoadingShows placeholder blocks and announces loading. | boolean | false |
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