Skip to content
UI

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
Open alerts1,2046% more than the previous 24 hoursLast 24 hours
Unassigned23Above the target of 10
Past SLA7Critical and high alerts
Time to resolve4h 12m38 minutes faster than the previous 30 daysLast 30 days
<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.

  • label and value arrive formatted: the label translated, the value formatted for the user’s locale (“1,204”, “4h 12m”). The tile does not format numbers.
  • unit sets a unit after the figure, smaller: value="92" unit="%".
  • delta shows the change against a previous period (see With a delta).
  • hint explains how the figure is computed, in a Help Tooltip.
  • meta is one line under the figure: the period, the comparison, the source. With a tone, it says why the figure needs attention.
  • trailing takes custom content on the figure’s line; prefer unit and delta.

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.

Resolved today58On track
Awaiting triage23Above the target of 10
Exposed critical assets3Internet-facing and unpatched
<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:

  • value is 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.
  • label is 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.

New alerts31218% fewer than the previous 7 daysCompared with the previous 7 days
Open vulnerabilities1,4869% more than the previous 7 daysCompared with the previous 7 days
Monitored assets2,031No change from the previous 7 daysCompared with the previous 7 days
<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.

Patch coverage92%Servers and workstations
Ingestion latency240ms95th percentile, last hour
Events ingested1,204,331Last 24 hours
<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.

Mean time to resolveNo valueNo alert was resolved in the last 30 days
Resolved alerts0Last 30 days
<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.

Monitored assets
Open vulnerabilities
Patch coverage
<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.

Unassigned23No owner yet
Past SLA7Critical and high
Escalated12To tier 2

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.label the sentence that says which way it moved.
  • tone never stands alone: its color and icon mark the meta text 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.

PropTypeDefault
labelstring

Already-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*string

The 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.

PropTypeDefault
deltaStatDeltaProps

Change against a previous period, shown beside the figure.

-
exactValuestring

The exact figure when `value` is abbreviated ("1.2M"): shown on hover and announced by screen readers instead of the abbreviation.

-
hintstring

Explanation of how the figure is computed, shown in a tooltip.

-
iconReactNode

Leading icon before the label.

-
isLoadingboolean

Renders skeletons in place of the figure and the meta line.

false
label*string

Already-translated label above the figure.

-
metaReactNode

Line 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
trailingReactNode

Custom content on the figure's baseline. Prefer `unit` and `delta`.

-
unitstring

Unit after the figure, set smaller: "%", "ms", "GB".

-
value*string | null

The 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.

-