Stat
A stat is one number with its label: a total, a count, a variation. It is the answer to “how many?”, where ScoreBar answers “how good?”. The package ships the class strings; the figure, the dash and every word come from the app.
Import
import { stat } from '@sportingscouter/ui';See ui/src/primitives/stat.ts for the slots, variants and the reasoning behind each decision.
Anatomy
- Average rating
- 4.7
- Up+0.2
- Rating vs 2025
| Slot | What it is |
|---|---|
group | One plane for the whole row, with hairlines between the cells. |
root | One cell: a raised surface with the density’s padding. |
label | The eyebrow voice: uppercase by CSS, never in the string, text-muted. |
value | The figure, tabular, in ink. Three sizes: sm, md (default), lg. |
delta | The signed variation: an arrow, a spoken direction and the figure. The figure is always ink. |
deltaIcon | The arrow, an icon the app passes. Only this slot takes the tone: positive, negative or neutral. |
deltaLabel | The spoken direction (“Up”, “Down”, “No change”), visually hidden, in the app’s language. |
note | Context for the figure, such as the period or what it is compared with. |
message | The error text, in ink. Only in the error state. |
action | The recovery action under the message, such as “Try again”. Only in the error state. |
skeletonValue | The loading placeholder for the figure: one value line tall, so the cell does not shift when the figure arrives. |
skeletonNote | The loading placeholder for the note. |
A row
Several organisations: four stats, columns: 4. One column on a phone, two from sm, four from lg.
- Companies
- 8
- Events
- 9
- Editions
- 14
- Verified reviews
- 2,543
Two stats, columns: 2: one column on a phone, two from sm.
- Editions
- 2
- Verified reviews
- 640
One organisation: three stats, columns: 3. One column until lg, so three stats never leave a filler cell.
- Events
- 1
- Editions
- 2
- Verified reviews
- 640
Alone, in a narrow cell: a long label wraps instead of overflowing.
- Days to race day
- 21
Sizes
- sm
- 2,543
- md (default)
- 2,543
- lg
- 2,543
The largest step is text-h1. text-display is marketing only and never a stat.
Delta: direction is not tone
The arrow follows the sign of the rounded delta, from deltaSign(). The tone is the app’s call: up is good for a rating and bad for disputes.
- Rating
- 4.7
- Up+0.2
- Up, positive
- Rating
- 4.5
- Down-0.2
- Down, negative
- Days to reply
- 3.2
- Up+0.4
- Up, negative
- Verified reviews
- 640
- No change0.0
- Flat, neutral
States
- Ready
- 2,543
- Verified reviews
- Loading
- Empty
- –
- Not enough reviews yet
Rules
columnsis the number of stats in the row. The hairlines are a 1px gap over abg-borderground, so they stay uniform at any width. An odd count in a two-column layout would leave one grey cell. Three stats usecolumns: 3; two usecolumns: 2; five or more are two rows, so two groups.- A missing number is a dash, never
0. Empty and error both show the app’s dash; they differ by content: error also carries a message and a retry action. Loading shows a skeleton and never a stale figure. - Small text is ink. The delta figure and the error message do not use a status colour: those tokens do not meet AA on the dark ground. The tone colours only the arrow, and direction is also in the sign, the arrow’s shape and the spoken
deltaLabel. - Direction and tone are separate. There is no
directionvariant: the arrow is an icon the app passes, fromdeltaSign(). - Tier, colour and bar width come from
roundRating(value), never from the raw value. When a stat shows a rating next to a ScoreBar or a tier, derive both fromroundRatingso the figure and the colour cannot disagree at a boundary:3.45is shown as3.5, which isvery-good. - A row is a
<dl>. Each cell is a<div>with a<dt>and one or more<dd>, so a screen reader says “Verified reviews, 2,543”. A lone stat can be a plain<div>. - No copy in the package. The label, the dash, the spoken direction and the retry text are the app’s, in the app’s locale.
Structure snippet
import { stat, formatCount, formatDelta, deltaSign } from '@sportingscouter/ui';
import { ArrowUp, ArrowDown, Minus } from 'lucide-react';
const s = stat();
const locale = 'pt-PT'; // from the app
/* The arrow and the spoken direction come from the sign of the rounded delta.
The tone is the app's call (up is good for a rating, bad for disputes), so it
is an input and never derived from the sign. */
const DIRECTION = {
up: { Icon: ArrowUp, label: 'Up' },
down: { Icon: ArrowDown, label: 'Down' },
flat: { Icon: Minus, label: 'No change' },
};
function VerifiedReviews({ count, change, tone }) { // tone is positive, negative or neutral
const shown = formatCount(count, locale); // "2 543", or null
const text = formatDelta(change, locale); // "+0,2", or null
const sign = deltaSign(change); // 'up' | 'down' | 'flat' | null
const direction = sign && DIRECTION[sign];
return (
<dl className={s.group({ columns: 1 })}>
<div className={s.root()}>
<dt className={s.label()}>Verified reviews</dt>
<dd className={s.value({ state: shown == null ? 'empty' : 'ready' })}>
{shown ?? '–'}
</dd>
{direction && text && (
<dd className={s.delta()}>
<direction.Icon className={s.deltaIcon({ tone })} aria-hidden="true" />
<span className={s.deltaLabel()}>{direction.label}</span>{text}
</dd>
)}
<dd className={s.note()}>Rating vs 2025</dd>
</div>
</dl>
);
}
// loading: s.root() with aria-busy, and <span className={s.skeletonValue()} />
// error: s.value({ state: 'error' }) + s.message() + s.action()theme.css.