Stat Card
Analytics tiles: a figure, how it moved, and one picture of its data, on a card or joined in a panel.
import { Stat, StatBars, StatBreakdown, StatBreakdownPart, StatCard, StatDelta, StatGroup, StatMeter, StatNote, StatValue } from '@youj/react'
Installation & usage
npm i @youj/reactFor an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.
Use stat tiles for the handful of numbers a dashboard opens with: revenue, orders, latency, accounts. Each one is built like an instrument readout: a small label, one big figure with its unit set small and muted, how it moved (a triangle and the change in the good or bad ink, then what it's compared against), and at most one picture of its data underneath. For a trend people need to read values from, use a Chart.
import { Stat, StatBars, StatDelta, StatGroup, StatMeter, StatValue } from '@youj/react'
<StatGroup aria-label="This week" className="sm:grid-cols-2">
<Stat label="Monthly recurring revenue">
<StatValue value={121140} format={{ style: 'currency', currency: 'USD', notation: 'compact', maximumFractionDigits: 1 }} />
<StatDelta value={0.064}>vs. last month</StatDelta>
<StatMeter value={121140} max={130000} marker={120000}>of the $130K target</StatMeter>
</Stat>
<Stat label="Orders" action={<span>7 days</span>}>
<StatValue value={1502} />
<StatDelta value={0.08}>vs. last week</StatDelta>
<StatBars data={[182, 204, 197, 221, 236, 214, 248]} previous={[171, 188, 201, 194, 210, 205, 219]} />
</Stat>
</StatGroup>Statis one tile: a group named by itslabel, with anactionat the end of the label's line (a period, a live marker, a small menu) andloading.StatGroupjoins tiles into one panel, split by hairlines at the theme's stroke. Any number of tiles wraps cleanly; set the columns withgrid-cols-*.StatValueis the figure.formattakesIntl.NumberFormatoptions, so currencies, percentages, compact numbers and units format in the YoujProvider's locale (en-US without one), or thelocaleyou pass;unitadds words Intl has no unit for ("seats").StatDeltais the change: a fraction by default (0.124is 12.4%), or in the value's own terms withformat. Its children are the comparison.StatNoteis a muted line for when there's no change to show.
Press Next reading in the preview: each figure rolls to its new value, each change counts to its own, the bars morph and a triangle turns when its change does. Refetch shows loading: the label stays, and the figures wait behind a skeleton.
Description
A dashboard of stat tiles for revenue, orders, latency and health that take a new live reading.
Give a tile the one picture its data calls for, drawn from real values:
StatBars: recent readings as thin bars on a baseline, the latest lit and the rest a quiet tint of itstone. Withprevious, last period's readings sit on each bar as ticks, so you see where this period beat it. Counts and sums read best as bars.StatMeter: a share of a whole, or progress to a target. The caption leads with the share;markerputs a notch on the track (where the pace says you should be by now), and the fill turns warning behind it and success once the target is met.StatBreakdown: how a total splits, one bar in parts from the inline start with a key of names and figures. A neutral part is the quiet gray, so the parts that need attention carry the color, and any part with a value keeps a sliver you can see.
For a rate or an average, a Sparkline reads better than bars.
Three stat cards, each with its own picture in the chart slot.
StatCard is a tile on its own Card, with its parts as props: value, format, delta (or previous, which it works the change out from), comparison and one picture in chart. It takes the Card's props too, spotlight included.
import { StatBars, StatCard } from '@youj/react'
<StatCard
label="Visitors"
value={48210}
previous={42891}
comparison="vs. last week"
chart={<StatBars data={[31, 33, 32, 36, 38, 37, 41, 44, 48]} tone="chart-1" />}
/>A churn rate and a cloud spend where a fall reads as good news.
For latency, churn and cost, a fall is good news. invert colors the change by the news (green when it fell), while the triangle still follows the number.
A list of deltas, up, down, inverted, flat and as a count.
StatDelta works on its own too, in a table cell or beside a heading.
Code
stat-card-demo.tsx, the Demo story. The component itself comes from the package.
Reference
Propslabel, action, loading… (51)
Props
Stat
| Prop | Type | |
|---|---|---|
labelRequired | ReactNodeWhat is measured: "Monthly recurring revenue". Names the tile. | – |
action | ReactNodeAt the end of the label's line: a period ("30 days"), a live marker, or a small menu button. | – |
loading | booleanThe figures and picture wait behind a skeleton; the tile is busy. default false | false |
loadingLabel | stringAnnounced while loading. Default "Loading {label}" when the label is a string. | – |
children | ReactNode | – |
Other props pass through: extends ComponentProps<'div'>. | ||
StatGroup
StatGroup takes Card's props. Its columns auto-fit at 11rem and up; set your own with grid-cols-*.
StatValue
| Prop | Type | |
|---|---|---|
valueRequired | number | – |
from | numberWhere the first roll starts. Default 0; pass the value itself to start settled and roll only on a change. | – |
format | Intl.NumberFormatOptionsIntl.NumberFormat options: currency, percent, compact notation, units. | – |
locale | stringDefault: the YoujProvider locale. | – |
unit | ReactNodeWords Intl has no unit for, set small and muted after the figure: "seats", "/mo". | – |
size | 'sm' | 'default' | 'lg'
default 'default' | 'default' |
Other props pass through: extends ComponentProps<'span'>. | ||
StatDelta
| Prop | Type | |
|---|---|---|
valueRequired | numberThe change: a fraction (0.124 = 12.4%) by default, or in the value's own terms with | – |
format | Intl.NumberFormatOptionsIntl.NumberFormat options for the change. default PERCENT | PERCENT |
invert | booleanDown is good for this metric (latency, churn, cost): a fall takes the good ink. default false | false |
locale | stringDefault: the YoujProvider locale. | – |
children | ReactNodeWhat it's compared against, muted after the change: "vs. last month". | – |
Other props pass through: extends ComponentProps<'span'>. | ||
StatBars
| Prop | Type | |
|---|---|---|
dataRequired | readonly number[]The readings, oldest first: one bar each. | – |
previous | readonly number[]Last period's readings, aligned with | – |
tone | StatTone'accent''neutral''success''warning''danger''chart-1''chart-2''chart-3''chart-4''chart-5'The lit bar's color; the rest are a quiet tint of it. default 'neutral' | 'neutral' |
highlight | number | 'none'Which bar is lit. Default: the latest. | – |
max | numberThe top of the scale. Default: the largest reading (or tick). | – |
label | stringAccessible name, e.g. "Orders per hour today, peaking at 11 am". Without it the bars are decorative. | – |
Other props pass through: extends ComponentProps<'div'>. | ||
StatMeter
| Prop | Type | |
|---|---|---|
valueRequired | number | – |
maxRequired | numberThe whole, or the target. | – |
marker | numberA notch on the track at this value: where the pace says you should be by now, or a limit inside a bigger whole. Behind it, the fill turns warning. | – |
tone | StatTone'accent''neutral''success''warning''danger''chart-1''chart-2''chart-3''chart-4''chart-5'The fill. Default | – |
segments | numberDraw this many cells instead of a solid bar. | – |
locale | stringDefault: the YoujProvider locale. | – |
children | ReactNodeThe words after the share: "of the $150K target". | – |
detail | ReactNodeAt the end of the caption: "$42K to go". | – |
Other props pass through: extends ComponentProps<'div'>. | ||
StatBreakdown
| Prop | Type | |
|---|---|---|
partsRequired | StatBreakdownPart[] | – |
format | Intl.NumberFormatOptionsIntl.NumberFormat options for each part's figure in the key. | – |
locale | stringDefault: the YoujProvider locale. | – |
legend | booleanThe key under the bar: each part's swatch, name and figure. default true | true |
Other props pass through: extends ComponentProps<'div'>. | ||
StatCard
| Prop | Type | |
|---|---|---|
labelRequired | ReactNodeWhat is measured: "Monthly recurring revenue". | – |
valueRequired | number | – |
format | Intl.NumberFormatOptionsIntl.NumberFormat options for the value: currency, percent, compact, units. | – |
locale | stringDefault: the YoujProvider locale. | – |
unit | ReactNodeWords Intl has no unit for, small and muted after the value: "seats". | – |
previous | numberThe previous period's value. Gives the change when | – |
delta | numberChange as a fraction (0.124 = +12.4%). Default: from | – |
invert | booleanDown is good for this metric (latency, churn, cost): a fall is shown as good news. default false | false |
comparison | ReactNodeThe period compared against, after the change: "vs. last month". Without a change it's a note. | – |
action | ReactNodeA slot beside the label: a period, a menu, a link. | – |
chart | ReactNodeA slot under the figures: StatBars, StatMeter, StatBreakdown or a Sparkline. | – |
loading | booleandefault false | false |
loadingLabel | stringAnnounced while loading. Default "Loading {label}" when the label is a string. | – |
spotlight | booleanLight the card with motion's Spotlight effect. | – |
variant | 'default' | 'inset' | 'panel'
| – |
lift | booleanRise on hover and focus: for cards that are one big target. | – |
size | 'default' | 'sm' | – |
StatCard passes the rest of its props to Card, at size="sm".
Right to leftLabels, figures and captions follow the page's direction; the…
Right to left
Labels, figures and captions follow the page's direction; the hairlines between tiles move to the inline end, and meters and breakdowns fill from the inline start. Bars and sparklines keep time running left to right.
MotionValue changes, change changes, change turns… · reduced motion
Motion
| Moment | Tokens | ||
|---|---|---|---|
| Value changesmotion’s Counter rolls each digit to the new number, a tight stagger step per place from the right | duration.deliberateease.emphasized560ms · cubic-bezier(0.6, 0, 0.1, 1) | 560mscubic-bezier(0.6, 0, 0.1, 1) | |
| Change changesthe figure counts to its new value | duration.slowease.standard340ms · cubic-bezier(0.2, 0, 0, 1) | 340mscubic-bezier(0.2, 0, 0, 1) | |
| Change turnsthe triangle turns to face the new direction | spring.snappy0.2s, bounce 0 | 0.2s, bounce 0 | |
| Bars arriveeach rises from the baseline in turn, left to right, when they scroll into view (a clip, so the rounded tops stay round) | duration.deliberateease.enter560ms · cubic-bezier(0.16, 1, 0.3, 1) | 560mscubic-bezier(0.16, 1, 0.3, 1) | |
| New readingsbars and ticks morph from where they are | duration.slowease.standard340ms · cubic-bezier(0.2, 0, 0, 1) | 340mscubic-bezier(0.2, 0, 0, 1) | |
| Meter fillsShareBar’s edge travels to the share; a new value retargets from where it is | spring.smooth0.36s, bounce 0 | 0.36s, bounce 0 | |
| Breakdown opensthe parts open from the inline start in turn; a new split glides | duration.slowease.standard340ms · cubic-bezier(0.2, 0, 0, 1) | 340mscubic-bezier(0.2, 0, 0, 1) | |
| Figures arriveContentSwap: rise out of a light blur while the height follows | duration.baseease.enter220ms · cubic-bezier(0.16, 1, 0.3, 1) | 220mscubic-bezier(0.16, 1, 0.3, 1) |
Reduced motion
Figures, bars, meters and breakdowns are drawn at their values: nothing rolls, rises or opens.
Reduced motion
Values swap to the new number without rolling, the triangle shows its new direction at once, bars, meters and breakdowns are drawn at their values, and loaded figures replace the skeleton without a fade or blur.
AccessibilityARIA, announcements
Accessibility
- Each tile is a group (
role="group") named by its label, so "Monthly recurring revenue, $121.1K, up 6.4% vs. last month" is read as one thing. StatDeltasays "Up", "Down" or "No change" in words before the figure. The triangle and its color are decorative.- The pictures are decorative by default: the figures and words beside them say the same. Give
StatBarsalabelto make it an image with a name ("Orders by day this week, rising").StatBreakdown's key is a list of each part's name and figure. - While
loading, a tile setsaria-busy, keeps its label, and announces "Loading" and the label (orloadingLabel); the new figures are read when they arrive. - An
actionneeds its own name, such asaria-label="Revenue options".