Stat list
A compact list of metrics, each with its value and its change.
Server-compatible: no hooks, no client boundary. Rows are driven by the items array rather than by children, so a whole panel of numbers is one prop. An item with an href becomes a link with a hover and a focus ring; the rest stay plain rows.
Install
$
npx shadcn@latest add @vibra/stat-listNeeds the @vibra registry in your components.json — set it up once.
Examples
import { StatList, type StatListItem } from "@/components/ui/stat-list"
// Churn is the one row where a rise is bad, so it sets positiveIsGood={false}
// and its +0.4% reads as danger while every other climb reads as success.
const ITEMS: StatListItem[] = [
{ label: "Visitors", value: "128,431", delta: 0.124 },
{ label: "Signups", value: "4,812", delta: 0.081 },
{ label: "Activated", value: "1,284", delta: -0.032 },
{ label: "Paid", value: "412", delta: 0.21 },
{ label: "Churned", value: "38", delta: 0.004, positiveIsGood: false },
]
export default function StatListDemo() {
return (
<div className="w-full max-w-sm panel px-6 py-2">
<StatList items={ITEMS} divided />
</div>
)
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
| items | StatListItem[] | — | One row each: label, value, and optionally delta, deltaFormat, positiveIsGood, icon, and href. |
| size | "sm" | "default" | "default" | sm tightens the rows and shrinks the deltas to match. |
| divided | boolean | false | Separates the rows with hairlines. |
Dependencies
Registry
Source
import * as React from "react"
import { cn } from "@/lib/utils"
import { MetricDelta, type MetricDeltaProps } from "@/components/ui/metric-delta"
export type StatListItem = {
label: React.ReactNode
/** Already formatted — pair it with the format lib. */
value: React.ReactNode
/** Change since the previous period; rendered as a <MetricDelta>. */
delta?: number
deltaFormat?: MetricDeltaProps["format"]
/** False for metrics where down is the win — churn, latency, cost. */
positiveIsGood?: boolean
/** Decorative icon before the label. Sized to 4 unless it sets its own size. */
icon?: React.ReactNode
/** Turns the row into a link to the metric's own view. */
href?: string
}
export type StatListProps = React.ComponentProps<"div"> & {
items: StatListItem[]
size?: "sm" | "default"
/** Separates the rows with hairlines. */
divided?: boolean
}
function StatList({
className,
items,
size = "default",
divided = false,
...props
}: StatListProps) {
return (
<div
data-slot="stat-list"
data-size={size}
data-divided={divided || undefined}
className={cn("flex w-full flex-col", divided && "divide-y", className)}
{...props}
>
{items.map((item, index) => {
const rowClassName = cn(
"flex items-center gap-3",
size === "sm" ? "py-2 text-xs" : "py-2.5 text-sm",
item.href &&
"rounded-md transition-colors hover:bg-muted/50 focus-ring"
)
const row = (
<>
{item.icon ? (
<span
data-slot="stat-list-icon"
className="shrink-0 text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
{item.icon}
</span>
) : null}
<span
data-slot="stat-list-label"
className="min-w-0 flex-1 truncate text-muted-foreground"
>
{item.label}
</span>
<span data-slot="stat-list-value" className="shrink-0 font-medium tabular-nums">
{item.value}
</span>
{item.delta !== undefined ? (
<MetricDelta
value={item.delta}
format={item.deltaFormat}
positiveIsGood={item.positiveIsGood}
size={size}
className="min-w-14 justify-end"
/>
) : null}
</>
)
// Two tags rather than a computed one, so the href stays typed as an
// anchor's own prop.
return item.href ? (
<a key={index} data-slot="stat-list-item" href={item.href} className={rowClassName}>
{row}
</a>
) : (
<div key={index} data-slot="stat-list-item" className={rowClassName}>
{row}
</div>
)
})}
</div>
)
}
export { StatList }