Skip to content

StatTile

Terminal window
$ npx shadcn@latest add https://lacodda.github.io/dowel/r/stat-tile.json
See it liveA row of figures under a heading, with and without the movement beneath them.→

It exists because the first consumer had written it twice. Once on the personal page, once on the team one — and the two copies had already drifted. One had grown a warning tone the other lacked, and in it the tone classes were joined without a space:

// The donor, and the defect waiting in it
className={`mt-1 font-mono text-lg tabular ${accent ? 'text-accent-2' : ''}${warn ? 'text-warn' : ''}`}

A figure that was both accented and warning would have emitted text-accent-2text-warn and been styled by neither. Nothing had gone wrong on screen, because those two flags were simply never passed together — which is the kind of defect that waits for the day they are.

A <dl>, for the reason KeyValue is one. Two loose <div>s read as two unrelated pieces of text, and nothing says the number belongs to the label.

<StatRow>
<StatTile label="Worked" value={duration(worked)} tone="accent" />
<StatTile label="Paused" value={duration(paused)} />
<StatTile label="Silent" value={String(silent)} tone="warn" icon={<TriangleAlert className="size-4" />} />
</StatRow>

The delta is a second line, not a colour on the number. A figure that turns red is a figure whose colour has to be explained, and the explanation is never on the screen. +40m vs last week explains itself.

A judgement brings a mark with it, and the type enforces that. warn and bad say the figure is itself a problem, and on a dashboard that claim rests on colour alone — which is where it fails. The line’s accents sit close to the status hues: measured in OKLab, dowel’s own accent is ΔE 6.1 from --warn, kasl-server’s gold is 3.7, hilvan’s is 2.5 from --bad. An accented tile and a warning one, three tiles apart in a row, are the same colour to a reader glancing down it.

Neither side can move — the status hues are the line’s shared language and the accents come from the brand registry — so the rule the theme states in prose becomes structure here: tone="warn" without an icon does not compile.

// Will not build: a judgement drawn in colour alone.
<StatTile label="Silent" value="3" tone="warn" />

The first consumer had already worked this out by hand, wrapping its value in a flex row with a warning triangle. That is the proof the requirement is real, and the reason it belongs in the primitive rather than in a guideline nobody reads twice. The mark is aria-hidden: it restates the tone, and the tone is already in the label’s words.

Its tone is stated rather than read off the sign, because the sign does not know what the figure means: for time to first response a fall is the good news.

<StatTile
label="Time to answer"
value="2m 10s"
delta="-30s vs last week"
deltaTone="good"
/>

An absent delta draws nothing, and a zero delta draws. A blank second line reads as “unchanged”, which is a claim about data the caller never made; 0 means the figure was measured and did not move. That is why the component tests undefined and null by name rather than reaching for a truthiness guard — {delta && …} swallows the one falsy value that means something.

Values are set in tabular figures, so a column of tiles lines up and a live number redrawn every few seconds does not shuffle the row sideways.

The row wraps rather than gridding. How many figures there are is decided at runtime — the donor hides two of its five until there is something to say — and a grid with a fixed column count leaves a hole where a hidden tile was. items-baseline is the part that is easy to miss: without it a tile carrying a delta stands taller than its neighbours, and the numbers stop sharing a line.

Props

Read from the types in ui/stat-tile.tsx, the file a product copies - so the description of each prop is also what an editor shows over it there.

StatTile

PropTypeDefault
sizemd | lg'md'
tonewith icondefault | accent | warn | badThe reading tone. default and accent carry no judgement, so they need no mark.
iconwith toneReactNodeA glyph before the figure. Optional here.
labelrequiredReactNodeWhat the figure is.
valuerequiredReactNodeThe figure. Already formatted - a duration, a count, a percentage: this component decides how a number looks, never what it says.
deltaReactNodeWhich way it moved, in words the caller chooses: +12% vs last week, 3 fewer than yesterday. Omitted when there is nothing to compare to - an empty line here reads as "unchanged", which is a claim.
deltaTonegood | bad | defaultWhether that movement is good news. Stated rather than read off the sign, because for a figure like time-to-answer a fall is the good direction.

And every prop of React's HTMLAttributes<HTMLDListElement> except title, passed on.

StatRow

Every prop of React's HTMLAttributes<HTMLDivElement>, passed on.

Accessibility

The examples on the stand, through axe 4.14.0 in a browser, in both themes, against WCAG 2.2 A and AA and axe's best practices: 7 rules applied and passed, and none failed. The accessibility report has every item.

History

Each release that changed ui/stat-tile.tsx - the file a product copies - newest first.

  1. v0.39.0
    • feat let the frame go on paper, and keep a panel or a chart on one sheet 354d583
  2. v0.28.0breaking
    • feat a judgement has to bring a mark, and the type says so ea09d66
  3. v0.24.0
    • feat one figure, and what it is a figure of e6a9466