Search

Jump to any page.

Data display

Stat

Key figure with change against the last period and a trend.

Conversations this week

2,431

+12.4%vs last week

Median time to cause

4h 12m

-31%vs last week

Repeat contacts

8.2%

+2.1%vs last week

pnpm dlx shadcn@latest add @paragon/stat
Brings in@paragon/sparklinelucide-reactcn
registry/paragon/components/stat.tsx
import * as React from "react"
import { TrendingDown, TrendingUp } from "lucide-react"
import { cn } from "cn"

import { Sparkline } from "@/registry/paragon/components/sparkline"

/**
 * A key figure with its change and trend. `delta` is a fraction (0.124 = +12.4%).
 * Set `good="down"` for metrics where falling is the win, such as
 * resolution time or repeat contacts.
 */
function Stat({
  label,
  value,
  delta,
  deltaLabel = "vs last week",
  good = "up",
  trend,
  locale = "en-GB",
  className,
  ...props
}: Omit<React.ComponentProps<"div">, "children"> & {
  label: React.ReactNode
  value: React.ReactNode
  delta?: number
  deltaLabel?: React.ReactNode
  good?: "up" | "down"
  trend?: number[]
  locale?: string
}) {
  const direction = delta == null || delta === 0 ? "flat" : delta > 0 ? "up" : "down"
  const outcome = direction === "flat" ? "neutral" : direction === good ? "good" : "bad"
  const Icon = direction === "down" ? TrendingDown : TrendingUp

  return (
    <div
      data-slot="stat"
      className={cn("flex flex-col gap-4 rounded-card bg-card p-5 ring-1 ring-border", className)}
      {...props}
    >
      <p className="text-sm text-muted-foreground">{label}</p>
      <div className="flex items-end justify-between gap-4">
        <p className="text-[2rem] leading-none font-medium tracking-[-0.04em] whitespace-nowrap tabular-nums">{value}</p>
        {trend ? (
          <Sparkline
            values={trend}
            area
            className={cn(
              "h-9 w-24 min-w-10 shrink",
              outcome === "good" && "text-oasis dark:text-[#a8cf9f]",
              outcome === "bad" && "text-destructive",
              outcome === "neutral" && "text-foreground/50"
            )}
          />
        ) : null}
      </div>
      {delta != null ? (
        <p className="flex items-center gap-1.5 text-sm text-muted-foreground">
          <span
            data-outcome={outcome}
            className="inline-flex items-center gap-1 font-medium tabular-nums data-[outcome=bad]:text-destructive data-[outcome=good]:text-oasis data-[outcome=neutral]:text-muted-foreground dark:data-[outcome=good]:text-[#a8cf9f]"
          >
            {direction !== "flat" ? <Icon aria-hidden="true" className="size-3.5 rtl:-scale-x-100" /> : null}
            {new Intl.NumberFormat(locale, {
              style: "percent",
              maximumFractionDigits: 1,
              signDisplay: "exceptZero",
            }).format(delta)}
          </span>
          {deltaLabel}
        </p>
      ) : null}
    </div>
  )
}

export { Stat }