Stat
Shows one key number and how it moved.
Examples
Tickets today
549
Up+10.2%vs 498 yesterday
Tickets today
1, 284
Up+7.9%vs 1,190 yesterday
Resolved by agents
78. 2 %
Up+0.4%vs 77.9
First reply
1. 4 s
Down−6.7%vs 1.5
Traffic
10 %
Reply time p95
1. 9 s
Near the 2s budget
Spend
$1, 284
Up+6.6%vs $1,204
Tickets today
549
Up+10.2%vs 498 yesterday
Sizes
- Runs549
defaultdefault
- Within SLA99.
98 % lg
- Pro$20
xl
Styles
- Agents12
plaindefault
- Releases38
framed
States
- Runs549Up+10.2%vs 498
up
- Runs412Down−17.3%vs 498
down
- Runs498No change0%vs 498
flat
- Agents3Upnewvs 0
new
- Runs549Up+10.2%vs 498
loading
- RefundsNone
No usage yet
empty
Usage
When to use it
- Use
- A view exists to answer how many or how much: spend, tickets, reply time.
- Use
- A row of two to four totals summarises the page below.
- Use
- A price or headline number on a marketing page: size xl.
- Not for
- The number is one cell among many of the same kind. Use Table.
- Not for
- The number is progress toward a limit. Use Progress.
- Not for
- The number is a count beside a label in a row. Use Badge.
- Not for
- The shape over time matters more than the latest value. Use Chart.
Rules
10 rules
- Must
- Pass from and to to StatDelta; never write the percent or arrow by hand. Because a typed "-8%" beside trend="up" puts the arrow against the figure and drops the "vs 507" baseline.
- Must
- Put a unit in StatUnit, not in the value string. Because StatValue breaks the string only after separators, so "412ms" sets the unit in the stat face at full size.
- Never
- Color a delta good or bad; say it in StatDescription. Because faster latency and more errors both go down, so a green or red delta tells half the readers the wrong thing.
- Must
- Set loading on every Stat in a row from one flag so they land together. Because tiles that resolve one by one shift the row and let the eye compare a new number with a stale one.
- Must
- Give a StatGroup role="group" and an aria-label naming the period. Because a screen reader reads four labels and values with no period, so "549" has no time frame.
- Must
- Put two or more stats in a StatGroup, never side by side in separate cards. Because a row of boxed numbers is the template dashboard, and each box adds two edges where one shared line does the job.
- Should
- Put two to four stats in a row and drop to two columns under 576px.
- Should
- Label with what is counted and the period in one to four words: Tickets today.
- Should
- Shorten a value past six characters with formatCounts (12.5M) or step down a size.
- Never
- Add a gradient, glow or count-up animation; use size lg or xl. Because a count-up shows numbers that were never true, and a reader who glances mid-count reads a wrong value.
Accessibility
5 checks
- Nothing in a Stat is a tab stop.
- The delta carries a sr-only word: Up, Down or No change.
- A new value (from 0) reads as the word new, not an infinite percent.
- The trend is role="img" named with the label and the trend in words; its sparkline and table are aria-hidden.
- The root sets aria-busy while loading.
Edge cases
9 edge cases
- 10-digit value at 160px
- Wraps between digit groups only; a long label wraps under itself.
- No value
- Write None in StatValue and say why in a StatDescription; from 0 to 0 draws no delta.
- Right to left
- Parts align to the start and the arrow mirrors.
- Zoom 200%
- The numeral doubles and wraps only between digit groups.
- Loading
- A 4ch block holds the value; delta and trend keep their space invisible.
- Value ticks four times a second
- Tabular figures keep the width; changes are not announced.
- NaN in from or to
- The delta renders nothing. On a failed request write None and say why.
- A group under 448px
- The cells stack, split by row rules instead of column rules.
- Forced colors
- The arrow stays currentColor; the frame edge draws in CanvasText, so the description carries trouble in words.
API
10 props
Stat size'default' | 'lg' | 'xl'default'default'Stat variant'plain' | 'framed'default'plain'Stat loadingbooleandefaultfalseStat kind, statusKind, 'warn' | 'fail'StatGroup kind, variantKind, 'framed' | 'plain'default'framed'StatDelta from, tonumberStatDelta format(n: number) => stringStatDelta periodstringStatDelta trend'up' | 'down' | 'flat'StatTrend data, labels, labelnumber[]
Tokens
20 tokens
--type-stat- 400 32px/36px var(--font-sans)
--type-stat-lg- 400 40px/44px var(--font-sans)
--type-stat-xl- 400 48px/52px var(--font-sans)
--features-data- "tnum", "zero", "ss04"
--type-label- 400 14px/16px var(--font-sans)
--text-2- oklch(var(--ink-c) / 0.80)
--type-body-lg- 340 16px/22px var(--font-sans)
--type-heading-sm- 400 20px/24px var(--font-sans)
--type-label-sm- 400 12px/16px var(--font-sans)
--text-3- oklch(var(--ink-c) / 0.66)
--line-frame- oklch(var(--ink-c) / var(--line-frame-alpha))
--line-row- oklch(var(--ink-c) / 0.08)
--surface-page- var(--paper)
--radius-frame- 10px
--surface-skeleton- color-mix(in oklch, oklch(var(--grey-c)), oklch(var(--ink-c)) var(--skeleton-step))
--accent-wash- var(--surface-selected)
--accent-mark- var(--ink)
--accent-line- var(--ink)
--warn-text- oklch(49% 0.100 68)
--fail-text- oklch(51% 0.180 26)