GitHub

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.

Stable .astro import EsaStat from '@esa/ecology/esa-stat.astro';

Preview

1,284
Active permits
<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.

1,284
Active permits
+12 this quarter
94%
On-time review
rolling 90 days
<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.

14
Monitored sites
3
Needs attention
27
Reports filed
<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.

14
Monitored sites
across 3 counties
1,284
Active permits
+12 this quarter
94%
On-time review
rolling 90 days
3
Needs attention
<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

PropTypeDefaultDescription
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.2M is announced as written. Prefer the full figure, or add an aria-label at the call site.
  • accent is decoration, not meaning — never let the tint be the only signal that a number is the alarming one. Say so in the label or sub.

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.

RoleRe-pointReach
.typography-body-sm
  • --typography-body-sm-font-family 'DM Sans', sans-serif via --typography-font-family-sans → --font-family-dm-sans
  • --typography-body-sm-font-size clamp(0.6875rem, 0.61rem + 0.38vw, 0.875rem) via --font-size-150
  • --typography-body-sm-font-weight 350 via --typography-font-weight-regular → --font-weight-350
  • --typography-body-sm-letter-spacing 0.01em via --letter-spacing-default
  • --typography-body-sm-line-height 1.6 via --line-height-normal
+20 more components esa-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
  • --typography-display-sm-font-family 'DM Sans', sans-serif via --typography-font-family-display → --typography-font-family-sans → --font-family-dm-sans
  • --typography-display-sm-font-size clamp(1.625rem, 1.41rem + 1.08vw, 2.25rem) via --font-size-700
  • --typography-display-sm-font-weight 650 via --typography-font-weight-bold → --font-weight-650
  • --typography-display-sm-letter-spacing -0.01em via --letter-spacing-tight
  • --typography-display-sm-line-height 1.3 via --line-height-tight
only here
.typography-label-md
  • --typography-label-md-font-family 'DM Sans', sans-serif via --typography-font-family-sans → --font-family-dm-sans
  • --typography-label-md-font-size clamp(0.75rem, 0.66rem + 0.44vw, 0.9375rem) via --font-size-200
  • --typography-label-md-font-weight 500 via --typography-font-weight-medium → --font-weight-500
  • --typography-label-md-letter-spacing 0.01em via --letter-spacing-default
  • --typography-label-md-line-height 1.6 via --line-height-normal
+33 more components esa-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