Stat
A single-metric block: a big display value, a readable label, and an optional muted caption. Replaces the bespoke .stat / .stat__value / .stat__label cluster every prototype re-implements.
Preview
<Stat value="1,284" label="Active permits" /> With a caption
The sub line is muted meta — a delta, a reporting period, a qualifier. Keep it short enough not to compete with the label.
<Stat value="1,284" label="Active permits" sub="+12 this quarter" />
<Stat value="94%" label="On-time review" sub="rolling 90 days" /> Accent
accent tints the value with --color-content-brand. Use it to
single out one figure in a row — a headline number among supporting ones. A row where
every stat is accented reads as no emphasis at all.
<Stat value="14" label="Monitored sites" />
<Stat value="3" label="Needs attention" accent />
<Stat value="27" label="Reports filed" /> Stat rows
The component is a bare flex column with a transparent background and no padding
of its own — the surface is the caller's job. Lay a row out with a layout primitive or a grid;
wrap it in an esa-card when the group needs
chrome. Deliberately no <esa-stat-group>: spacing between stats belongs to
the page, not to the metric.
<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 2rem;">
<Stat value="14" label="Monitored sites" sub="across 3 counties" />
<Stat value="1,284" label="Active permits" sub="+12 this quarter" />
<Stat value="94%" label="On-time review" sub="rolling 90 days" />
<Stat value="3" label="Needs attention" accent />
</div> API
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | number | required | The metric itself — rendered large, in the display face. |
label | string | required | What the value counts. Sentence case, readable body size — not an uppercase overline. |
sub | string | — | Optional muted caption under the label (a delta, a period, a qualifier). |
accent | boolean | false | Tints the value with the brand secondary emphasis. Off by default keeps it primary text. |
Accessibility
- Renders plain
<div>s — no heading semantics. The value is not an<h*>, so a stat row never disturbs the page's heading outline. - The value comes before the label in the DOM, which is also the reading order screen readers announce. Keep the label self-describing ("Active permits", not "Active") so the pair makes sense read aloud.
- Abbreviate carefully:
1.2Mis announced as written. Prefer the full figure, or add anaria-labelat the call site. accentis decoration, not meaning — never let the tint be the only signal that a number is the alarming one. Say so in the label orsub.
Theming surface
Every public token this component reads, extracted from its source at build time,
split by blast radius: the hooks that belong to this component,
then the shared and system tokens it consumes alongside everything else. The
Resolves to column walks each token's real lineage —
component → semantic → primitive → raw value — so you
can see exactly where re-pointing it lands. A spoke re-skins by overriding these in
its theme-<slug>.css; component internals are never edited. No
hook for what you need? File it with /request-lego.
Type roles it names
This component does not set a size, weight, face or tracking of its own — it names a composite, and the composite carries all five. A composite is a class rather than a token, so none of it appears in the tables below; re-theme text by re-pointing the token family, not by editing the component.
| Role | Re-point | Reach |
|---|---|---|
.typography-body-sm |
| +20 more componentsesa-alert-box, esa-breadcrumbs, esa-button-toggle, esa-card, esa-checkbox-group, esa-combobox, esa-command-palette, esa-date-picker, esa-empty-state, esa-file-upload, esa-form-field, esa-input-tag, esa-link-column, esa-loading-overlay, esa-progress-bar, esa-radio-group, esa-search-panel, esa-select, esa-text-field, esa-textarea |
.typography-display-sm |
| only here |
.typography-label-md |
| +33 more componentsesa-app-bar, esa-breadcrumbs, esa-button-group, esa-button-toggle, esa-card, esa-checkbox-group, esa-color-picker, esa-combobox, esa-command-palette, esa-confirm-dialog, esa-danger-zone, esa-date-picker, esa-dialog, esa-empty-state, esa-entity-search, esa-file-upload, esa-filter-container, esa-form-field, esa-header-nav, esa-input-tag, esa-link-column, esa-page-header, esa-pillbox, esa-popover, esa-progress-bar, esa-radio-group, esa-range-slider, esa-select, esa-side-dialog, esa-sidebar-nav, esa-text-field, esa-textarea, esa-tooltip |
Wired to this component
No other component reads these — re-point them and esa-stat is the
only thing that moves.
| Token | Tier | Resolves to |
|---|---|---|
--stat-accent-color | component | #2a7e3b via --color-content-brand → --color-grass-11 |
--stat-value-color | component | #202020 via --color-content-default → --color-gray-12 |
Shared tokens it reads
Owned by a family or by the semantic layer. Still overridable — but scope the
override (esa-foo { --form-border-color: … }) unless you mean to move
everything that reads it. Reach says what else that is.
| Token | Reach | Resolves to |
|---|---|---|
--color-content-default-secondary | semantic every component | #646464 via --color-gray-11 |
--typography-body-sm-font-size | semantic every component | clamp(0.6875rem, 0.61rem + 0.38vw, 0.875rem) via --font-size-150 |
--typography-display-sm-font-family | semantic every component | 'DM Sans', sans-serif via --typography-font-family-display → --typography-font-family-sans → --font-family-dm-sans |
--typography-display-sm-font-size | semantic every component | clamp(1.625rem, 1.41rem + 1.08vw, 2.25rem) via --font-size-700 |
--typography-display-sm-font-weight | semantic every component | 650 via --typography-font-weight-bold → --font-weight-650 |
--typography-label-md-font-size | semantic every component | clamp(0.75rem, 0.66rem + 0.44vw, 0.9375rem) via --font-size-200 |
--typography-label-md-font-weight | semantic every component | 500 via --typography-font-weight-medium → --font-weight-500 |
--spacing-050 | primitive every component | 0.125rem |