Agent docs

StatTile

The dashboard number tile: icon, label, one big figure, optional trend.

The dashboard number tile: an icon, a label, one big figure, and optional trend, hint, and click-through. "Every stats header in the product family re-invents this shape; this is the one implementation" (stat-tile.tsx:15-18).

The canon behind it. method/design-language/05-cards-and-surfaces.md §A6 (05-cards-and-surfaces.md:32, StatTile is for a single entity's summary row, not for lists) and method/design-language/09-page-composition.md §A4 (09-page-composition.md:25, composition order is fixed, stat tiles sit between PageTitle and the page's cards).

Toolkit sibling. packages/toolkit/src/text.ts (pluralize) and number.ts (getHumanReadableNumber) are the count-copy helpers a stat tile's value and label lean on; the toolkit README's module table names this doc as where text belongs (packages/toolkit/README.md, "Belongs with").

Narrow tiles. A tile is its own container. Below about 12rem of tile width the figure steps from text-3xl to text-2xl, the label wraps to two lines instead of truncating, and the trend label drops under the figure, so a seven-digit number in a two-column phone grid is never cut off. This works wherever the tile sits and needs no prop; the tile therefore needs a width from its parent (a grid track or flex-1), as it always has.

Laws it enforces#

  • Every composite declares its narrow behaviour: the tile scales its own content instead of squishing it.
  • Fit, then truncate, never clip: a label wraps to two lines before it is cut.

When to reach for it#

Two shapes are sanctioned, and both summarize ONE entity.

  • A single record's summary row. A job's Examined, verb, Sent to review and Duration across grid-cols-2 md:grid-cols-4 (speedway/app/routes/workspaces/jobs/run.tsx:413-437).
  • An account-level dashboard row. Still one entity, the account rather than a list of records: speedway/app/routes/workspaces/workspace.tsx:301-344, walmart-mvp/frontend/src/pages/Home.tsx:176-207 and Admin.tsx:729-738.

What the boundary excludes is a list. Canon doc 5 §A6: "StatTile is for a single entity's summary row, not for lists. ... Lists put numbers in columns instead." The same section records the dashboard row being stripped from speedway on 2026-07-29 and it has since returned, which is why both shapes are named here.

Contract#

  • label (required) / value (required): value gets font-mono text-3xl font-semibold tabular-nums (text-2xl in a narrow tile) when it is a string or number via renderNode; pass JSX to opt out. label wraps to two lines.
  • Icon / iconSize (default 16) / iconClassName (IconImportProps): the icon sits in a small tinted tile whose background is bg-current/10, so it derives its color from whatever color/toneClassName or iconClassName sets, "one prop colors icon and tile" (stat-tile.tsx:64-67 comment).
  • trend ("up" | "down" | "flat") / trendLabel: a small colored arrow-and-text pair beside the value (up success, down error, flat muted). No shipped usage found in either app.
  • hint: a muted line under the value row, in the tile's footer.
  • onClick / href / linkAs / arrow: makes the tile interactive. href wins over onClick when both are given, and renders a real link (hover-prefetch, middle-click, copy-address all work) via linkAs (a router link component) instead of a plain <a>. arrow draws a right-arrow at the footer's right edge as the "this tile goes somewhere" affordance; when toneClassName (or color) is also set, the arrow crossfades from grey to the tile's tone on hover using two stacked icons, because a computed group-hover: class on one icon would be purged by the build (stat-tile.tsx:96-106 comment). No shipped usage of onClick found; every interactive tile in both apps uses href (+ linkAs, + arrow) instead.
  • tooltip: wraps the whole tile in the kit Tooltip. The tile is rendered inside a display: grid wrapper specifically so the tooltip's own trigger <div> does not shrink-wrap the tile out of its grid track, keeping wrapped and bare tiles laid out identically (stat-tile.tsx:134-139).
  • color (SemanticColor): themes the icon tile, the value, and the arrow's hover color together from the shared palette (the color axis, canon 14-prop-vocabulary.md). toneClassName is the raw text-color-class escape hatch (e.g. "text-warning") for a color the palette does not carry; color wins over it. Explicit iconClassName/valueClassName still win where they overlap.
  • loading: data-slot loading. The label, icon, and hint render as themselves; only the value (and the hint, if none was given) become skeleton bars. This is distinct from skeleton below: a tile with loading still shows its statically-known text.
  • skeleton: renders a whole tile-shaped placeholder (icon-tile bar, label bar, value bar, footer bar), for a row whose tiles are not yet individually known.
  • noAnimate: opts out of the animate-fadeInUp entrance the tile plays by default on mount (mirrors Card's noAnimate; skeleton mode never animates).
  • Slot classnames per StatTileClassNames: className, valueClassName, labelClassName.

Sanctioned combinations#

CombinationProducesWhere usedWhy
Icon + iconClassName + valueClassName + hint + arrow + tooltip + href + linkAsA dashboard KPI tile that is fully clickable, colour-themed, and explains its own destination on hoverspeedway/app/routes/workspaces/workspace.tsx:301-312The richest real usage: identity color, a hover explainer naming the destination page, and a deep link in one tile
Icon + toneClassName + valueClassName + hint + href + arrow (no tooltip)A dashboard KPI tile themed by outcome (success/warning/info/accent) without a hover explainerwalmart-mvp/frontend/src/pages/Home.tsx:177-186toneClassName alone is enough when the label and hint already say where the number leads
loading with static label, hint, Icon, toneClassName, and value=""A KPI row that shows its own shape (icon, label, hint) immediately while only the number is still arrivingwalmart-mvp/frontend/src/pages/Admin.tsx:773, Home.tsx:80-83Doc 4 §A3's "never blank what you already know" applied at the tile level: everything known ahead of the fetch stays visible
skeleton alone, looped for a fixed tile countA KPI row whose entire shape (not just the values) is unknown yetspeedway/app/routes/workspaces/workspace.tsx:229Used ahead of loading in the same page: the loop renders bare skeleton tiles before any label/hint text is available at all
A single tile wrapped in an external <Tooltip> (not the tile's own tooltip prop), explaining one anomalous number in an otherwise plain rowA one-off explanatory hover on the one tile that needs itspeedway/app/routes/workspaces/jobs/run.tsx:427-437The Duration tile's recoveries count needs "a recovery is the worker restarting after an interruption" explained only when recoveries > 0; the other three tiles in the same row carry no tooltip at all

Banned combinations#

Do not use StatTile as a per-row element inside a list, "in place of table columns" per canon doc 5 §A6. The dashboard KPI row (an account-level count) is a distinct, sanctioned shape from a per-list-item tile; the rule targets the latter.

Do not set both onClick and href; href silently wins, and since real usage always reaches for href + linkAs for navigation, an onClick-only tile is untested territory in either app.

Do not size an unexplained number's tile without a hint or a tooltip. Doc 5 §A6: "A tile explains an odd number via Tooltip rather than an unexplained hint," citing the same Duration/recoveries case above.

Before you adopt this#

Five questions to answer before reaching for StatTile.

  1. Does the shell, a parent layout, or a global provider already render this? Not applicable, StatTile is page content; no shell already renders it.
  2. Does this app already ship a local implementation of the same thing? Not applicable, no local StatTile clone is documented in either app.
  3. Does this app's kit pin reach the version this component or prop landed in? The narrow-width scaling arrives with the next kit bump after 0.5.0; no app change is needed on upgrade.
  4. Does the component derive its own accessible name and keyboard path, or must the call site supply them? href silently wins over onClick; the link itself, not a click handler, supplies navigation.
  5. Which canon §A rules bind this surface, and which does the composition break? §A6 reserves StatTile for one entity's summary, never in place of table columns.

Travels with#

Tooltip, both as the tile's own tooltip prop and as an external wrap for a single anomalous tile in a row (see the two distinct patterns above; they are not interchangeable, the tile's own prop wraps the whole tile including its click target, an external wrap does the same but lives at the call site instead of inside the component).

PageTitle, which the tile row follows directly in the fixed composition order (canon doc 9 §A4: PageTitle → stat tiles → cards). A StatTile row is a sibling of Card, not nested inside one, per card.md's own "travels with" note.

Snippet#

import { StatTile } from "@versable-git/ui";
// speedway/app/routes/workspaces/workspace.tsx:301-312
<StatTile
Icon="Review"
iconClassName="text-warning"
label="Assigned to you"
value={d.reviewCount.toLocaleString()}
valueClassName="text-warning"
hint="parts in your review queue"
arrow
tooltip={<GoToPage page="review" />}
href={`/workspaces/${wid}/review`}
linkAs={RowLink}
/>
@versable-git/ui · reference, canon, and method, read in place