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. docs/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 docs/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").

When to reach for it#

Canon doc 5 §A6 states the boundary directly: "StatTile is for a single entity's summary row, not for lists. ... Lists put numbers in columns instead." The doc's cited reference case is speedway/app/routes/workspaces/jobs/run.tsx:413-437, a single job's summary row (Examined / a module-specific verb / Sent to review / Duration), grid-cols-2 md:grid-cols-4.

Both apps also ship a second, wider shape: an account-level KPI row on the workspace dashboard, still one entity (the account, not a list of rows) being summarized. speedway/app/routes/workspaces/workspace.tsx:301-344 renders four tiles (Assigned to you / Jobs in progress / Ready to export / Onboarded this month), and walmart-mvp/frontend/src/pages/Home.tsx:176-207 and Admin.tsx:729-738 do the same for their own dashboards. Canon doc 5 §A6 separately notes the dashboard KPI row concept "was deliberately stripped [from speedway] 2026-07-29 ("hide this info for now, may return")"; the current workspace.tsx code (last touched 2026-08-05) shows it did return. Treat both shapes, the single-record detail row and the account-level dashboard row, as sanctioned; the boundary the canon is drawing is against using StatTile as a stand-in for table columns, not against a dashboard summarizing an account.

Contract#

  • label (required) / value (required): value gets font-mono text-3xl font-semibold tabular-nums when it is a string or number via renderNode; pass JSX to opt out.
  • 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 tone 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 tone 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).
  • tone: one text-color class (e.g. "text-warning") that themes the icon tile, the value, and the arrow's hover color together. Explicit iconClassName/valueClassName still win over it 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 + tone + 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-186tone alone is enough when the label and hint already say where the number leads
loading with static label, hint, Icon, tone, 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? Not applicable, no version-landed StatTile prop is noted in this doc.
  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#

// 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