Stat tiles
Summary
One KPI per tile: a small uppercase label, a big light-weight
value, and an optional delta line. Adding
.soma-stat-up or .soma-stat-down to the
delta colors it and prepends a chevron glyph — but the direction
always also lives in the text (+14%,
−8), so color is never the sole channel.
Tiles carry no layout of their own — they are flex-column content
blocks you compose into a .soma-grid inside a
widget, which is how dashboard KPI rows are built.
When to use
| Pattern | Use it for |
|---|---|
| Tile per KPI | The headline dashboard numbers: one metric each, scannable at a glance. |
-up / -down delta | Movement against a baseline. Keep the sign in the text; the glyph and color reinforce it. |
| Neutral delta | Context that isn't a movement, such as a period ("last 30 days") or a qualifier. Omit both direction classes. |
| Table instead | Many metrics to compare or sort — a grid of tiles stops scanning well past a handful. |
Examples
HTML
The minimal tile is label over value; the delta line is optional:
<div class="soma-stat">
<span class="soma-stat-label">Neurons active</span>
<span class="soma-stat-value">18</span>
</div>
Directional deltas: the sign lives in the text, the class adds
color and a chevron; the nested <small>
carries the comparison context:
<span class="soma-stat-delta soma-stat-up">+14% <small>vs last week</small></span>
<span class="soma-stat-delta soma-stat-down">−8 <small>since yesterday</small></span>
A neutral delta (no direction class) is plain muted text for context that isn't a movement:
<span class="soma-stat-delta">last 30 days</span>
In the dashboard KPI row, tiles carry soma-col-*
classes inside a .soma-grid, all in one widget:
<div class="soma-grid">
<div class="soma-stat soma-col-3">
<span class="soma-stat-label">Visits today</span>
<span class="soma-stat-value">4,332</span>
<span class="soma-stat-delta soma-stat-up">+14% <small>vs last week</small></span>
</div>
<!-- more tiles … -->
</div>
CSS classes
| Class | Effect |
|---|---|
.soma-stat | The tile: a flex column; give it grid columns (soma-col-*) for layout. |
.soma-stat-label | Small uppercase muted label above the number. |
.soma-stat-value | The number: display size, light weight, tight line height. Set in the mono stack with tabular figures, so live values don't jitter as digits tick and stat columns align; .soma-numeric applies the same treatment to consumer-rendered numerics. |
.soma-stat-delta | Small delta row under the value; a nested <small> carries the comparison context. Muted (neutral) without a direction class. |
.soma-stat-up / .soma-stat-down | On the delta: success/danger text color plus a 12px chevron glyph. Omit both for a neutral delta. |
Delta semantics
Three channels say the same thing: the sign in the text
(+14%), the chevron glyph, and the color. The text
is the source of truth (it is what gets read aloud, copied and
translated), so never strip the sign because the arrow "already
shows it". The glyph is CSS (an icon mask filled
with currentColor, no markup span needed), and the
colors come from the -subtle-fg tokens, which are
tuned as text colors in every theme — dark greens/reds
in light mode, lifted tints in dark — so the delta always meets
AA on the widget surface.
Direction is movement, not judgment: -down on
"Open tickets −8" is a good week. Pick the class by which way
the number moved; let the label and context copy say whether
that's welcome. When a metric has no baseline to move against,
use a neutral delta (or none at all) rather than forcing a
direction.
Composing into grids
A tile is deliberately layout-free, so the same markup works at
any width: give each tile a soma-col-* class inside
a .soma-grid. Four soma-col-3 tiles
make the classic KPI row, six soma-col-2 a dense
strip, or soma-col-6 pairs above a chart. Keep one
widget per row of related KPIs (a single
soma-widget whose body holds the grid) rather than
a widget per number, so the row scans as one glance. Under 900px
the grid collapses to a single column and the tiles stack.
A tile also composes inside a richer widget: a
.soma-stat above a
.soma-chart gives the
headline number over its trend — the standard dashboard panel.
JavaScript
None — stat tiles are CSS-only. Refreshing is the consumer's concern: update the three text slots and swap the direction class when the movement changes. All in one place, so the number, sign and color never disagree:
function renderStat(tile, { value, delta, context }) {
tile.querySelector('.soma-stat-value').textContent = value;
const d = tile.querySelector('.soma-stat-delta');
d.classList.toggle('soma-stat-up', delta > 0);
d.classList.toggle('soma-stat-down', delta < 0);
const sign = delta > 0 ? '+' : delta < 0 ? '−' : '';
d.textContent = `${sign}${Math.abs(delta)}% `;
const small = document.createElement('small');
small.textContent = context; // e.g. 'vs last week'
d.appendChild(small);
}
const data = await fetch('/api/kpis').then((r) => r.json());
renderStat(document.querySelector('#kpi-visits'), data.visits);