Stats

Weekly Support KPI Card

Compact weekly support metrics card with six figures in two columns, each with a change arrow and comparison text, kept level by a subgrid. Worsening changes show in red.

Free, MIT licenseOpen in Browse
import { ArrowDownIcon, ArrowUpIcon } from "@phosphor-icons/react/dist/ssr"
import { cn } from "cn"

import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"

// The copy: change text here, not in the markup.
const COPY = {
  title: "Support this week",
  description: "Oct 19–25, against Oct 12–18",
  against: "than last week",
} as const

type Metric = {
  label: string
  value: string
  note?: string
  change: { direction: "up" | "down"; text: string; worse?: boolean }
}

// The week opened with 57 tickets: 57 + 132 new − 141 solved leaves 48.
// Satisfaction is 49 good ratings out of 52.
const METRICS: Metric[] = [
  {
    label: "Open",
    value: "48",
    change: { direction: "down", text: "9 fewer" },
  },
  { label: "New", value: "132", change: { direction: "up", text: "14 more" } },
  {
    label: "Solved",
    value: "141",
    change: { direction: "up", text: "22 more" },
  },
  {
    label: "First reply",
    value: "1h 12m",
    note: "median",
    change: { direction: "down", text: "8m faster" },
  },
  {
    label: "Resolution",
    value: "9h 40m",
    note: "median",
    change: { direction: "up", text: "1h 5m slower", worse: true },
  },
  {
    label: "Satisfaction",
    value: "94%",
    note: "of 52 ratings",
    change: { direction: "up", text: "2 pts higher" },
  },
]

export default function StatsCard01() {
  return (
    <Card className="mx-auto w-full max-w-sm">
      <CardHeader>
        <CardTitle>
          <h2>{COPY.title}</h2>
        </CardTitle>
        <CardDescription>{COPY.description}</CardDescription>
      </CardHeader>
      <CardContent>
        {/* Volume runs down the first column, speed and satisfaction down the
            second. Each cell spans three of the nine rows (label, value,
            change), so the lines stay level across a row even when one wraps;
            add a row of cells and the grid needs three more rows. */}
        <dl className="grid grid-flow-col grid-cols-2 grid-rows-[repeat(9,auto)] gap-x-4 gap-y-6">
          {METRICS.map((metric) => {
            const Arrow =
              metric.change.direction === "up" ? ArrowUpIcon : ArrowDownIcon
            return (
              <div
                key={metric.label}
                className="row-span-3 grid min-w-0 grid-rows-subgrid gap-y-1"
              >
                <dt className="text-xs text-muted-foreground">
                  {metric.label}
                </dt>
                <dd className="flex flex-wrap items-baseline gap-x-1">
                  <span className="text-xl font-medium tabular-nums">
                    {metric.value}
                  </span>
                  {metric.note ? (
                    <span className="text-xs text-muted-foreground">
                      {metric.note}
                    </span>
                  ) : null}
                </dd>
                <dd
                  className={cn(
                    "flex items-center gap-1 text-xs",
                    metric.change.worse
                      ? "text-destructive"
                      : "text-muted-foreground"
                  )}
                >
                  <Arrow aria-hidden="true" className="size-3 shrink-0" />
                  <span>
                    {metric.change.text}
                    <span className="sr-only"> {COPY.against}</span>
                  </span>
                </dd>
              </div>
            )
          })}
        </dl>
      </CardContent>
    </Card>
  )
}

Installation

pnpm dlx shadcn add @01orbital/stats-card-01
npx shadcn add @01orbital/stats-card-01
yarn shadcn add @01orbital/stats-card-01
bunx --bun shadcn add @01orbital/stats-card-01

Free under the MIT license. Add the @01orbital registry to components.json once (install guide), then run the command.

It lands at components/blocks/stats/stats-card-01/index.tsx, as source you own and edit.

Usage

import StatsCard01 from "@/components/blocks/stats/stats-card-01"

export default function Example() {
  return <StatsCard01 />
}

Dependencies

npm: @phosphor-icons/react, cn. The CLI installs them for you.

Registry pieces it installs with it:

  • card