Stat
Big-number display with a label, optional icon, trend delta (▲ ▼), tone-coloured value, and three sizes. Wraps CountUp automatically when given a numeric count, so the value animates in on mount. Pair with StatGroup to lay out a row with optional vertical dividers.
Usage
import { Stat, StatGroup } from '@bwo-ui/react';
<Stat label="Sites shipped" count={420} />
<Stat label="Uptime" count={99.9} decimals={1} suffix="%" />
<Stat label="Version" value="v2.0" />
<Stat
label="Monthly visitors"
count={128400}
delta={12.3}
deltaSuffix="%"
deltaLabel="vs last month"
/>Delta indicator
Pass delta as a positive or negative number to render a trend line below the value — ▲ for up, ▼ for down, → for zero. Colour follows the sign by default (positive → green, negative → red). For metrics where a lower number is better — bounce rate, error rate, page load time — pass goodWhen="down" to flip the colour mapping so a falling number reads as success.
{/* Higher is better (default) — positive delta is green */}
<Stat label="Monthly visitors" count={128400} delta={12.3} deltaSuffix="%" deltaLabel="vs last month" />
{/* Lower is better — negative delta is green */}
<Stat
label="Bounce rate"
count={38}
suffix="%"
delta={-2.1}
deltaSuffix=" pp"
deltaLabel="vs last month"
goodWhen="down"
/>Sizes
sm (24 px value) for table cells and dense dashboards, md (44 px, default) for general use, lg (56 px) for hero positions like the top of a report or a single-stat callout.
smmdlg<Stat size="sm" label="Monthly visitors" count={128400} delta={12.3} deltaSuffix="%" />
<Stat label="Monthly visitors" count={128400} delta={12.3} deltaSuffix="%" />
<Stat size="lg" label="Monthly visitors" count={128400} delta={12.3} deltaSuffix="%" />With icons
Pass an inline SVG via icon to add a leading visual category cue. Keep icons at about 20 × 20 px and use currentColor so they pick up the surrounding muted colour automatically.
<Stat icon={<UsersIcon />} label="Active users" count={4910} />
<Stat icon={<DollarIcon />} label="MRR" count={48230} prefix="$" />
<Stat icon={<CartIcon />} label="Orders" count={372} />
<Stat icon={<BoltIcon />} label="API calls" count={1.24} decimals={2} suffix=" M" />Tone
Colour the value itself when the stat's state itself is meaningful — green when a target's been hit, yellow as a warning, red as a problem. Tone applies only to the value; delta colours continue to follow goodWhen.
<Stat tone="default" label="tone=default" count={4910} />
<Stat tone="success" label="tone=success" count={4910} />
<Stat tone="warning" label="tone=warning" count={4910} />
<Stat tone="danger" label="tone=danger" count={12} />Alignment
align controls how the value, label, and delta line up within the Stat's own box. start (default) is the natural left-rail layout; center is for symmetric dashboard cards; end right-aligns everything for a table's last column or a number column in a report.
<Stat align="start" label="Start" count={420} delta={5} deltaSuffix="%" />
<Stat align="center" label="Center" count={420} delta={5} deltaSuffix="%" />
<Stat align="end" label="End" count={420} delta={5} deltaSuffix="%" />StatGroup
Wrap multiple Stats in a StatGroup to lay them out as a row. The divided prop adds a 1 px vertical line between each item — useful for report headers and the "at a glance" row at the top of a dashboard.
Plain groupdivided{/* Plain row */}
<StatGroup>
<Stat label="Visitors" count={12834} />
<Stat label="Conversions" count={420} />
<Stat label="Revenue" count={48230} prefix="$" />
</StatGroup>
{/* With vertical dividers between items */}
<StatGroup divided>
<Stat label="Visitors" count={12834} delta={8.2} deltaSuffix="%" />
<Stat label="Conversions" count={420} delta={-1.4} deltaSuffix="%" />
<Stat label="Revenue" count={48230} prefix="$" delta={12.7} deltaSuffix="%" />
</StatGroup>Dashboard recipe
The canonical dashboard layout: a 4-up grid of Card-wrapped stats. Use size="sm", icons for visual scanning, deltas with appropriate goodWhen, and tone="danger" on the error counter so problems stand out.
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 12 }}>
<Card pad="compact">
<Stat size="sm" icon={<UsersIcon />} label="Active users" count={4910}
delta={6.2} deltaSuffix="%" />
</Card>
<Card pad="compact">
<Stat size="sm" icon={<DollarIcon />} label="MRR" count={48230} prefix="$"
delta={12.7} deltaSuffix="%" />
</Card>
<Card pad="compact">
<Stat size="sm" icon={<CartIcon />} label="Bounce rate" count={38} suffix="%"
delta={-2.1} deltaSuffix=" pp" goodWhen="down" />
</Card>
<Card pad="compact">
<Stat size="sm" icon={<BoltIcon />} tone="danger" label="Errors" count={37}
delta={9} goodWhen="down" />
</Card>
</div>Hero stat
Single, oversized stat for the top of a report. Combine size="lg"with a contextual badge or status pill on the side.
<Card>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<Stat
size="lg"
label="Total revenue this quarter"
count={482310}
prefix="$"
delta={18.4}
deltaSuffix="%"
deltaLabel="vs last quarter"
/>
<Badge variant="green" size="sm">Live</Badge>
</div>
</Card>Accessibility
- The label is rendered after the value but is the most important text for screen readers — keep it specific ("Monthly visitors", not just "Visitors").
- The delta arrow (
▲ ▼ →) is decorative and renderedaria-hidden. The numeric value and thedeltaLabelcarry the actual meaning. - Don't rely on colour alone for delta direction. The arrow itself is part of the message — a colour-blind user reading "▲ 12.3 % vs last month" still gets the trend.
CountUpanimates the value on mount. The animation respectsprefers-reduced-motion: reduceat the CSS / GSAP layer (the underlying factory honours the OS setting).- For live-updating stats (real-time data), wrap the dashboard region in an element with
aria-live="polite"so screen readers announce changes when the number ticks.
Stat props
| Prop | Type | Default | Description |
|---|---|---|---|
label | ReactNode | — | Caption — what the number measures. |
count | number | — | Numeric value — animates from 0 via `CountUp`. Use `value` for non-numeric. |
value | ReactNode | — | Static value for non-numeric stats (e.g. `"v2.0"`). Mutually exclusive with `count`. |
decimals | number | 0 | Decimal places — used by `count` and the `delta` display. |
prefix | string | — | Prepended to the count (e.g. `"$"`). |
suffix | string | — | Appended to the count (e.g. `"%"`, `" ms"`). |
hint | ReactNode | — | Optional small sub-line under the label. |
icon | ReactNode | — | Leading inline SVG. Renders `aria-hidden`; ~20 × 20 px. |
size | 'sm' | 'md' | 'lg' | 'md' | 24 / 44 / 56 px value font. |
align | 'start' | 'center' | 'end' | 'start' | Content alignment within the stat. |
tone | 'default' | 'success' | 'warning' | 'danger' | 'default' | Colour the value itself for state-driven stats. Does not affect the delta colour. |
delta | number | — | Trend amount. Renders `▲`/`▼`/`→` based on sign; colour follows `goodWhen`. |
deltaPrefix | string | — | Prepended to the delta value (e.g. `"$"`). |
deltaSuffix | string | — | Appended to the delta value (e.g. `"%"`, `" pp"`). |
deltaLabel | ReactNode | — | Context text after the delta — e.g. `"vs last month"`. |
goodWhen | 'up' | 'down' | 'up' | Whether higher or lower is good. Drives delta colour. Use `"down"` for bounce rate, error rate, load time, etc. |
…rest | HTMLAttributes<HTMLDivElement> | — | Native div attributes are forwarded. |
StatGroup props
| Prop | Type | Default | Description |
|---|---|---|---|
divided | boolean | false | Render a 1 px vertical line between each child stat. |
size | 'sm' | 'md' | 'lg' | — | Adjusts the gap between items. Children still set their own `size` individually. |
…rest | HTMLAttributes<HTMLDivElement> | — | Native div attributes are forwarded. |