Stat
Puts a single number and its label at display size, so the figure lands before the explanation.
When to Use
Use when:
A number is the point — hero proof figures, dashboard KPIs, a session's headline metrics. Use
ws_stats() for a row of two to four related figures.
Variants
Single stat
ws_stat() renders one figure. Value and label are both positional and both required.
50+
Workshops
120+
Exercises
1,000+
Facilitators
<?= ws_stat('50+', 'Workshops') ?>
<?= ws_stat('120+', 'Exercises', ['color' => 'var(--phase-learn)']) ?>
<?= ws_stat('1,000+', 'Facilitators', ['icon' => 'groups']) ?>Stat group
ws_stats() lays out a row with dividers between items. It takes the same per-item keys as the single helper.
50+
Workshops
120+
Exercises
80+
Icebreakers
1,000+
Facilitators
<?= ws_stats([
['value' => '50+', 'label' => 'Workshops'],
['value' => '120+', 'label' => 'Exercises'],
['value' => '80+', 'label' => 'Icebreakers'],
['value' => '1,000+', 'label' => 'Facilitators'],
]) ?>With colour
Colour applies to the value only. Pass a token reference rather than a literal hex so the figures follow the palette.
50+
Workshops
120+
Exercises
80+
Icebreakers
<?= ws_stats([
['value' => '50+', 'label' => 'Workshops', 'color' => 'var(--phase-learn)'],
]) ?>With icons
Apply icons to the whole row or to none of it — a partly-iconed group reads as broken.
50+
Workshops
120+
Exercises
80+
Icebreakers
<?= ws_stats([
['value' => '50+', 'label' => 'Workshops', 'icon' => 'school'],
]) ?>States
| State | Behavior |
|---|---|
| Default | The only state. Stats are static text with no hover, focus, or interactive treatment. |
| Zero | Renders "0" at full display size — visually as loud as a large number. When zero means "nothing yet", Empty State communicates better. |
| Long values | No truncation or auto-fitting. A long value widens the item and can unbalance a group, so pre-format big numbers ("1.2k", not "1,247"). |
| Single item in a group | ws_stats() with one item renders without dividers — safe, but ws_stat() is the clearer call. |
Real-World Usage
The hero proof row on a landing page: counts pulled live from the library so the marketing figures can't drift from the database.
120+
Exercises
80+
Icebreakers
50+
Workshop templates
<?= ws_stats([
['value' => $counts['exercises'] . '+', 'label' => 'Exercises'],
['value' => $counts['icebreakers'] . '+', 'label' => 'Icebreakers'],
['value' => $counts['workshops'] . '+', 'label' => 'Workshop templates'],
]) ?>Hero takes a stats option in the same shape, so inside a hero you don't call this component directly.
Options
ws_stat()
| Option | Type | Default | Purpose |
|---|---|---|---|
$value | string | '' | Positional. The figure, pre-formatted — "50+", "1,000+", "98%". |
$label | string | '' | Positional. What the figure counts. Plural nouns read best. |
color | string | null | Value colour. Accepts a hex or a CSS variable — prefer var(--phase-learn). |
icon | string | null | Material icon above the value |
id | string | null | Element ID |
class | string | '' | Additional CSS classes |
ws_stats()
| Option | Type | Default | Purpose |
|---|---|---|---|
$stats | array | required | Positional array of items, each accepting value, label, color, and icon |
id | string | null | Container ID |
class | string | '' | Additional CSS classes on the container |
Accessibility
| Concern | Behavior |
|---|---|
| ARIA | None applied. Value and label are adjacent text read in order — "50 plus, Workshops" — which is the correct reading without any extra wiring. |
| Reading order | The value comes before the label in the DOM as well as visually, so the announcement matches what's on screen. |
| Keyboard | Not focusable. Stats are output, not controls. |
| Abbreviations | A value like "1.2k" is read literally and may not be understood. Spell it out in the label when precision matters, or pass the full number. |
| Not a heading | The value renders large but isn't a heading element, so it won't pollute the page outline — deliberate. Don't wrap it in one to get the size. |
| Contrast | Values default to --text-dark and labels to --text-muted. A custom color bypasses that check — verify it against the surface. |
Tokens
| Token | Used for |
|---|---|
--text-dark | Default value colour |
--text-muted | Label colour |
--gray-300 | Divider between items in a group |
--text-h2 / --text-h3 | Value display size |
--text-caption | Label size |
--font-heading | Fraunces on the value |
--font-body / --font-bold | Inter on the label, and the value's weight |
--space-1 / --space-4 / --space-6 | Value-to-label gap and the spacing between group items |
CSS Classes
| Class | Purpose |
|---|---|
.ws-stat | Single stat wrapper |
.ws-stat__value | The figure — display size, heading face |
.ws-stat__label | The caption beneath |
.ws-stat__icon | Optional icon above the value |
.ws-stats | Group container (flex row) |
.ws-stats__divider | Vertical rule between group items |
Files
| File | Purpose |
|---|---|
includes/components/helpers.php | ws_stat() and ws_stats() helper functions |
includes/components/stat.php | Single stat template |
includes/components/stats.php | Group template — loops items and inserts dividers |
includes/components/components.css | Styles (.ws-stat and .ws-stats rules) |