ComponentsStat Tile
Stat Tile
The number first, then what changed, then how it got here. Group tiles into a KPI row that wraps cleanly from phone to wide dashboard.
On this page
Install component source from the public registry, or use the local workspace package. Follow the setup guide first. The npm package is not published yet.
View registry filesImplemented with React and browser APIs, without an additional behavior or rendering library.
Shared foundation: React, clsx for conditional classes, tailwind-merge for resolving utility-class conflicts. Styles use Tailwind CSS and Dethink CSS variables.
Based on this component’s source and registry composition. Libraries used only in showcase examples are excluded.
Import the component into your page or component file.
import { KpiGroup, StatTile } from "@dethink/components";A joined KPI panel, linked cards with compact trends, and loading, missing and large-value states.
KPI row
A joined panel of four metrics. Churn and latency set positiveDirection to down, so an increase in latency reads as a regression.
Show sourceexamples/stat-tile/dashboard-row.tsx
"use client";
import { KpiGroup, StatTile } from "@dethink/components";
export function StatTileDashboardRow() {
return (
<KpiGroup
aria-label="Workspace health, last 30 days"
variant="joined"
className="w-full"
>
<StatTile
label="Monthly recurring revenue"
value={128_430}
formatOptions={{ style: "currency", currency: "USD" }}
delta={8.2}
comparison="vs last month"
trend={[92, 96, 95, 101, 104, 103, 109, 113, 112, 118, 124, 128]}
trendColor="chart-1"
/>
<StatTile
label="Active workspaces"
value={12_408}
delta={3.1}
comparison="vs last month"
trend={[
10.9, 11, 11.2, 11.1, 11.4, 11.6, 11.7, 11.9, 12, 12.1, 12.3, 12.4,
]}
trendColor="chart-3"
/>
<StatTile
label="Logo churn"
value="1.8%"
delta={{ value: -0.4, positiveDirection: "down" }}
comparison="vs last month"
trend={[2.6, 2.5, 2.7, 2.4, 2.3, 2.2, 2.4, 2.1, 2, 2.2, 1.9, 1.8]}
trendColor="chart-2"
/>
<StatTile
label="API p95 latency"
value={182}
formatValue={(value) => `${value} ms`}
delta={{
value: 6.5,
positiveDirection: "down",
}}
comparison="vs last week"
trend={[168, 171, 166, 170, 174, 169, 172, 176, 175, 179, 177, 182]}
trendColor="chart-7"
/>
</KpiGroup>
);
}Linked tiles
Separate cards that navigate to a detail view, with the trend beside the value. A tiny change below the threshold reads as flat.
Show sourceexamples/stat-tile/separate-tiles.tsx
"use client";
import { KpiGroup, StatTile } from "@dethink/components";
import { CreditCard, ShoppingBag, Users } from "lucide-react";
export function StatTileSeparate() {
return (
<KpiGroup
aria-label="Store performance today"
minTileWidth="15rem"
className="w-full"
>
<StatTile
href="#orders"
icon={<ShoppingBag />}
label="Orders"
value={1_842}
delta={12.4}
comparison="vs yesterday"
trend={[40, 52, 48, 61, 58, 72, 80, 76, 91, 104]}
trendVariant="bar"
trendColor="chart-1"
trendPlacement="end"
/>
<StatTile
href="#customers"
icon={<Users />}
label="New customers"
value={318}
delta={-2.3}
comparison="vs yesterday"
trend={[36, 35, 35.5, 34, 33.5, 33, 32, 32.5, 31.5, 31]}
trendVariant="line"
trendColor="chart-5"
trendPlacement="end"
/>
<StatTile
href="#payments"
icon={<CreditCard />}
label="Payment success"
value="99.2%"
delta={{ value: 0.05, neutralThreshold: 0.1 }}
comparison="vs yesterday"
caption="Within the 99% target"
trend={[98.6, 98.8, 98.7, 98.9, 99, 99.1, 99, 99.2]}
trendVariant="line"
trendColor="chart-6"
trendPlacement="end"
/>
</KpiGroup>
);
}Loading, missing and large values
Skeletons keep the layout while loading. Missing data shows a dash with an explanation instead of a zero.
Data resumes after tonight's sync
Show sourceexamples/stat-tile/states.tsx
"use client";
import { useState } from "react";
import { Button, KpiGroup, StatTile } from "@dethink/components";
export function StatTileStates() {
const [loading, setLoading] = useState(true);
return (
<div className="grid w-full gap-4">
<KpiGroup aria-label="Tile states" className="w-full">
<StatTile
label="Net revenue"
value={48_210}
formatOptions={{ style: "currency", currency: "USD" }}
delta={5.6}
comparison="vs last week"
trend={[31, 35, 34, 39, 41, 44, 48]}
loading={loading}
size="sm"
/>
<StatTile
label="Refund rate"
value={null}
delta={null}
comparison="vs last week"
caption="Data resumes after tonight's sync"
size="sm"
/>
<StatTile
label="Annual contract value"
value={2_400_000}
formatOptions={{ style: "currency", currency: "USD" }}
delta={18}
comparison="year over year"
size="lg"
/>
</KpiGroup>
<div>
<Button
size="sm"
variant="outline"
aria-pressed={loading}
onClick={() => setLoading((v) => !v)}
>
{loading ? "Finish loading" : "Show loading"}
</Button>
</div>
</div>
);
}| Prop | What it does | Default |
|---|---|---|
labelReactNode | Sentence-case metric name without a trailing colon. Names the tile's group. | Not set |
valuenumber | string | null | Numbers format compactly (1,284 · 12.9K · $4.2M); strings render as given; null shows a dash. | Not set |
formatValue / formatOptions(value) => string / Intl.NumberFormatOptions & { locale } | Replace the formatter, or tune it with currency, percent and locale options. | compact |
deltanumber | DeltaBadgeProps | A signed change, or full DeltaBadge props such as positiveDirection. | Not set |
comparisonstring | Shown beside the delta and included in its announcement, e.g. "vs last month". | Not set |
trend / trendColor / trendVariantChartValue[] / ChartColor / "line" | "area" | "bar" | Recent values drawn as a Sparkline. | — / "chart-1" / "area" |
trendPlacement"bottom" | "end" | Full-width under the value, or compact beside it. | "bottom" |
trendLabelstring | Accessible name for the trend when the label is not text. | "{label} trend" |
icon / captionReactNode | Decorative icon beside the label; supporting text under the value. | Not set |
loadingboolean | Keeps the layout and swaps content for skeletons; sets aria-busy. | false |
href / target / relstring | Render the tile as a link. New-tab links get noopener noreferrer. | Not set |
interactiveboolean | Hover and focus styling when you wrap the tile in your own router link. | false |
size"sm" | "md" | "lg" | Scales the value and trend height. | "md" |
| Prop | What it does | Default |
|---|---|---|
variant"separate" | "joined" | Separate cards with a gap, or one framed panel with hairline dividers. | "separate" |
minTileWidthstring | Narrowest tile width before the row wraps. Tiles stretch to fill each row. | "13rem" |
aria-label / aria-labelledbystring | When present, the row becomes a named group. | Not set |
Each tile is a group named by its label. The delta is announced as a sentence such as “Up 8.2% vs last month”, so the visible comparison is not read twice. The trend is an image with a summary of its range. Linked tiles are a single link whose name includes the label and value. Wrap a tile in your router’s link and set interactive to get the same hover and focus treatment. Loading tiles set aria-busy and keep their size, so nothing shifts when data arrives.