Gesso
Data display

Stat strip

Metrics joined into one object, each a label, a tabular number, and a hint.

Stat strip draws a row of numbers that belong together, like a campaign's reach, as one object instead of a card per number. Each stat is a label, a number, and an optional hint that puts the number in context, and the strip reads the same on a page, a card, or a tray. For a single number on its own card, use Card's CardMetric.

Recipients
1,204
Delivered
1,148
95% of recipients
Opened
493
43% of delivered
Clicked
88
8% of delivered
Sent
1,204
Bounced
41
3% of sent
Unsubscribed
6
<1% of sent

Usage

import { Stat, StatStrip } from '@/components/ui/stat-strip';

<StatStrip>
  <Stat label="Delivered" value={delivered.toLocaleString()} hint="95% of recipients" />
  <Stat label="Opened" value={opened.toLocaleString()} hint="43% of delivered" />
</StatStrip>

value takes any node, so format numbers the way the rest of the app does. Put each Stat directly inside the strip, because the strip counts its children to decide how many columns to draw.

One object, drawn by the gap

Each stat is a tile on the sunken well (bg-surface-sunken), and the strip sets the tiles apart by the stroke width (--sys-stroke, 1px or 2px with the look's UI weight), so the surface underneath draws the dividers. The strip clips its corners to rounded-lg. Description list draws its rows the same way. A tile is a 14px label in text-fg-muted, a number in text-heading-3 with tabular figures, and a 12px hint in text-fg-muted, also tabular.

Layout

From sm up the strip takes one equal column per stat. Below sm it drops to two a row, and an odd last stat spans the row so the strip stays square. Keep a strip to about five stats, and past that split it into two strips under their own Section titles. The breakpoint follows the window, not the strip's own width, so a strip in a narrow column on a wide screen stays in one row. Narrow the window to see the three-stat strip above put its last stat on a row of its own.

Props

StatStrip takes every <dl> attribute, and Stat takes every <div> attribute, plus these on Stat.

PropTypeDefaultWhat it does
labelReactNodeWhat the number counts, drawn above the number.
valueReactNodeThe number, already formatted.
hintReactNodeA line of context under the number, like "41% of recipients". It is left out when empty.

Accessibility

  • The strip is a <dl> and each stat a <div> holding a <dt> for the label and a <dd> for the value, so a screen reader reads each number with its label. A hint is a second <dd> in the same group.
  • Every figure is text, so a screen reader gets the values as written. Color does not carry any meaning, since the tiles share one tint.
  • A strip is not focusable and announces nothing when a value changes, so wrap it in a live region yourself if numbers update while a person is looking at it.
  • Nothing moves, so reduced motion has nothing to turn off.
  • In forced colors the tint and the surface become the same system background, so each tile draws a 1px CanvasText outline into the gap and the strip a border, which keeps the dividers.

When to use something else

  • Card with CardMetric for one number that stands on its own.
  • Description list for label and value pairs that read as a record, not as headline numbers.
  • Sparkline when the shape of a number over time matters more than the number, placed beside a stat.
  • Segmented bar when the numbers are parts of one whole and the split is the point.
  • Progress for one value moving toward a total.

Install

shadcn add @gesso/stat-strip

On this page