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

PatternUse it for
Tile per KPIThe headline dashboard numbers: one metric each, scannable at a glance.
-up / -down deltaMovement against a baseline. Keep the sign in the text; the glyph and color reinforce it.
Neutral deltaContext that isn't a movement, such as a period ("last 30 days") or a qualifier. Omit both direction classes.
Table insteadMany metrics to compare or sort — a grid of tiles stops scanning well past a handful.

Examples

Plain

Label + value only:

Neurons active 18

The delta line is optional. Label + value alone is a valid tile.

Up delta

soma-stat-up · success + chevron:

Visits today 4,332 +14% vs last week

The + stays in the text; glyph and color only reinforce it. The nested <small> carries the comparison context.

Down delta

soma-stat-down · danger + chevron:

Open tickets 27 −8 since yesterday

"Down" is not "bad" — the classes color the movement, your copy gives it meaning.

Neutral delta

No direction class:

Uptime 99.98% last 30 days

Omit both direction classes for context that isn't a movement (a period or qualifier). Muted text, no glyph.

Dashboard grid

Tiles composed into a soma-grid:

Visits today 4,332 +14% vs last week
Sign-ups 312 +3% vs last week
Open tickets 27 −8 since yesterday
Uptime 99.98% last 30 days

The KPI row: tiles get soma-col-* classes and sit in a .soma-grid inside one widget. Under 900px the grid collapses to a single column.

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

ClassEffect
.soma-statThe tile: a flex column; give it grid columns (soma-col-*) for layout.
.soma-stat-labelSmall uppercase muted label above the number.
.soma-stat-valueThe 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-deltaSmall delta row under the value; a nested <small> carries the comparison context. Muted (neutral) without a direction class.
.soma-stat-up / .soma-stat-downOn 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);