Stat Tile
A card with one figure, for the KPI row at the top of a dashboard or list.
@zenoxai/ui/patterns/stat-tile Markdown<div className="grid w-full grid-cols-2 gap-3 md:grid-cols-4">
<StatTile
label="Open alerts"
value="1,204"
hint="Alerts not yet resolved or dismissed, across all tenants."
delta={{
value: "6%",
trend: "up",
sentiment: "negative",
label: "6% more than the previous 24 hours",
}}
meta="Last 24 hours"
/>
<StatTile
label="Unassigned"
value="23"
tone="warning"
meta="Above the target of 10"
/>
<StatTile
label="Past SLA"
value="7"
tone="danger"
meta="Critical and high alerts"
/>
<StatTile
label="Time to resolve"
value="4h 12m"
hint="Median time from alert creation to resolution."
delta={{
value: "38m",
trend: "down",
sentiment: "positive",
label: "38 minutes faster than the previous 30 days",
}}
meta="Last 30 days"
/>
</div>Usage
import { StatTile } from "@zenoxai/ui/patterns/stat-tile"
export function AlertKpis() {
return (
<div className="gap-3 md:grid-cols-4 grid grid-cols-2">
<StatTile label="Open alerts" value="1,204" meta="Last 24 hours" />
<StatTile
label="Past SLA"
value="7"
tone="danger"
meta="Critical and high"
/>
</div>
)
}
Every KPI row is built from StatTile, so figures have the same size and weight
(text-2xl, semibold, tabular figures) on every screen. Lay the tiles out in a grid; each tile
fills the height of its row.
labelandvaluearrive formatted: the label translated, the value formatted for the user’s locale (“1,204”, “4h 12m”). The tile does not format numbers.unitsets a unit after the figure, smaller:value="92" unit="%".deltashows the change against a previous period (see With a delta).hintexplains how the figure is computed, in a Help Tooltip.metais one line under the figure: the period, the comparison, the source. With atone, it says why the figure needs attention.trailingtakes custom content on the figure’s line; preferunitanddelta.
The figure is never colored. Color on the number says “bad” without saying why, turns a row of tiles into a traffic light, and makes the most prominent text on the screen the hardest to read. State goes on the reason instead.
The label truncates to one line. Keep it short, and keep a row to four tiles or fewer so labels fit at common widths.
Examples
Needs attention
tone reports that the figure needs attention now: a queue above its target, items past their
SLA. The figure keeps its color; the meta line, which says why, takes the tone’s color and
icon (a triangle for warning, an octagon for danger), so the state is always read with its
reason. Write meta as that reason: “Above the target of 10”, not “Warning”.
tone is a state, not a category. A tile that counts critical vulnerabilities stays default
while the count is within its target; marking it because the category is “critical” teaches
users to ignore the marks.
<div className="grid w-full grid-cols-1 gap-3 sm:grid-cols-3">
<StatTile label="Resolved today" value="58" meta="On track" />
<StatTile
label="Awaiting triage"
value="23"
tone="warning"
meta="Above the target of 10"
/>
<StatTile
label="Exposed critical assets"
value="3"
tone="danger"
meta="Internet-facing and unpatched"
/>
</div>With a delta
delta shows the change against a previous period, on a soft chip with an arrow:
valueis the formatted change (“18%”, “38m”).trend(up,down,flat) sets the arrow.sentiment(positive,negative,neutral, the default) sets the color. Only the app knows whether a move is good news: fewer alerts is positive, fewer patched hosts is negative. The color follows the sentiment, never the direction.labelis the sentence screen readers announce instead of the bare value: “18% fewer than the previous 7 days”.
Name the period in meta. Hide the delta when there is nothing to compare with, rather than
showing “0%” for missing data. The chip is also exported as StatDelta, for a change shown
beside other figures.
<div className="grid w-full grid-cols-1 gap-3 sm:grid-cols-3">
<StatTile
label="New alerts"
value="312"
delta={{
value: "18%",
trend: "down",
sentiment: "positive",
label: "18% fewer than the previous 7 days",
}}
meta="Compared with the previous 7 days"
/>
<StatTile
label="Open vulnerabilities"
value="1,486"
delta={{
value: "9%",
trend: "up",
sentiment: "negative",
label: "9% more than the previous 7 days",
}}
meta="Compared with the previous 7 days"
/>
<StatTile
label="Monitored assets"
value="2,031"
delta={{
value: "0%",
trend: "flat",
label: "No change from the previous 7 days",
}}
meta="Compared with the previous 7 days"
/>
</div>Units and abbreviated figures
unit keeps the unit out of the figure, smaller and in secondary text, so a row of
percentages and milliseconds still lines up on the digits. Abbreviate large figures (“1.2M”)
only in a tile, and pass the exact figure in exactValue: it shows on hover and screen readers
announce it instead of the abbreviation.
<div className="grid w-full grid-cols-1 gap-3 sm:grid-cols-3">
<StatTile
label="Patch coverage"
value="92"
unit="%"
meta="Servers and workstations"
/>
<StatTile
label="Ingestion latency"
value="240"
unit="ms"
meta="95th percentile, last hour"
/>
<StatTile
label="Events ingested"
value="1.2M"
exactValue="1,204,331"
meta="Last 24 hours"
/>
</div>No data
value={null} shows an Empty Value when there is nothing to count,
such as a mean time with no resolved alert. Never show 0 for missing data: zero is a figure,
and the second tile above is right to show it.
<div className="grid w-full grid-cols-1 gap-3 sm:grid-cols-2">
<StatTile
label="Mean time to resolve"
value={null}
meta="No alert was resolved in the last 30 days"
/>
<StatTile label="Resolved alerts" value="0" meta="Last 30 days" />
</div>Loading
isLoading replaces the figure, and meta when it is set, with skeletons of the same height.
The label stays, so the tile keeps its size and the user knows what is coming.
<div className="grid w-full grid-cols-2 gap-3 md:grid-cols-3">
<StatTile
label="Monitored assets"
value=""
meta="All tenants"
isLoading
/>
<StatTile label="Open vulnerabilities" value="" isLoading />
<StatTile label="Patch coverage" value="" isLoading />
</div>Clickable
Pass onClick to make the whole tile clickable, usually to open the list filtered to what the
figure counts. The tile stretches a real button over the card and keeps the hint’s own button
above it, so onClick and hint work together without nesting one button inside another.
Select a tile to filter the list.
function StatTileClickable() {
const [filter, setFilter] = React.useState<string | null>(null)
return (
<div className="flex w-full flex-col gap-3">
<div className="grid grid-cols-2 gap-3 md:grid-cols-3">
<StatTile
label="Unassigned"
value="23"
tone="warning"
meta="No owner yet"
onClick={() => setFilter("Unassigned alerts")}
/>
<StatTile
label="Past SLA"
value="7"
tone="danger"
meta="Critical and high"
onClick={() => setFilter("Alerts past SLA")}
/>
<StatTile
label="Escalated"
value="12"
meta="To tier 2"
onClick={() => setFilter("Escalated alerts")}
/>
</div>
<p className="text-sm text-text-secondary">
{filter ? `Showing: ${filter}` : "Select a tile to filter the list."}
</p>
</div>
)
}Accessibility
- A clickable tile is a native button covering the card, in the tab order, activated with Enter and Space. Its accessible name is the label and the figure with its unit (“Patch coverage: 92%”), or the exact figure when one is given; the hint stays a separate button.
- Screen readers read the figure as formatted. An abbreviated figure needs
exactValue. - A delta’s arrow is decorative: give
delta.labelthe sentence that says which way it moved. tonenever stands alone: its color and icon mark themetatext that explains it.
API reference
Generated from the TypeScript source. className, style and children are accepted everywhere and omitted below.
StatDelta
StatDelta is the change of a figure against a previous period: an arrow for the direction and the value, on the soft fill of its sentiment. The color says whether the change is good news, not which way it went, so a drop in open alerts and a rise in coverage both read green. StatTile renders it from its `delta` prop; use it alone beside other figures.
| Prop | Type | Default |
|---|---|---|
label | stringAlready-translated sentence screen readers announce instead of the bare value, such as "18% fewer than the previous 7 days". | - |
sentiment | "neutral" | "positive" | "negative"Whether the move is good news. Only the app knows: fewer alerts is positive, fewer patched hosts is negative. Defaults to `neutral`. | neutral |
trend* | "flat" | "up" | "down"Which way the figure moved. Drives the arrow, never the color. | - |
value* | stringThe change, already formatted for the user's locale ("18%", "+42"). | - |
StatTile
StatTile is the single-figure card of a summary band: a label, one figure at the KPI step of the type scale (text-2xl, semibold, tabular figures), and optionally a unit, a delta, a hint and a meta line. Every KPI row uses it, so figures read the same size and weight on every screen.
| Prop | Type | Default |
|---|---|---|
delta | StatDeltaPropsChange against a previous period, shown beside the figure. | - |
exactValue | stringThe exact figure when `value` is abbreviated ("1.2M"): shown on hover and announced by screen readers instead of the abbreviation. | - |
hint | stringExplanation of how the figure is computed, shown in a tooltip. | - |
icon | ReactNodeLeading icon before the label. | - |
isLoading | booleanRenders skeletons in place of the figure and the meta line. | false |
label* | stringAlready-translated label above the figure. | - |
meta | ReactNodeLine under the figure: the period, the comparison, the source. With a `tone`, the reason the figure needs attention. | - |
onClick | (() => void)Makes the whole tile a button, usually to open the filtered list. | - |
tone | "default" | "warning" | "danger"Whether the figure needs attention: a queue above its target, items past their SLA. The figure stays neutral; the meta line takes the tone's color and icon, so the state is read together with its reason. | default |
trailing | ReactNodeCustom content on the figure's baseline. Prefer `unit` and `delta`. | - |
unit | stringUnit after the figure, set smaller: "%", "ms", "GB". | - |
value* | string | nullThe figure, already formatted for the user's locale ("1,204", "4h 12m"). `null` means there is no data, and the tile shows an empty value instead of a misleading zero. | - |