Skip to content
ssntl/ui

Stat

A big number with a short label and a signed change, where importance shows in size, weight and color.

Monthly visitors

24,608

+2,140 since September

Refunds

38

+6 since September

Installation

$ pnpm dlx shadcn@latest add @ssntl/stat

Usage

tsx
import { Stat, StatDelta, StatLabel, StatValue } from "@/components/ui/stat"

<Stat>
  <StatLabel>Monthly visitors</StatLabel>
  <StatValue>24,608</StatValue>
  <StatDelta trend="up">+2,140 since September</StatDelta>
</Stat>
  • Importance: the value leads in size (48px, lg 64px), weight (bold) and color (foreground); label and note step back to muted.
  • Beyond size: hierarchy comes from weight and color too, so the number still leads in a small card.
  • Write the sign and the period in the delta; green and red only support the words.
  • Peer stats share size, weight and label position. Tabular figures keep them aligned.

Code

components/ui/stat.tsx
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/lib/utils"

// Essential Design `es-stat`: when the number is the point, it gets the space.
// Importance shows in more than size: the value is large, heavy and full
// foreground; the label and the note step back in caption size and muted
// color. Tabular figures so peer stats line up.
const statValueVariants = cva("font-bold tabular-nums", {
  variants: {
    size: {
      md: "text-headline",
      lg: "text-stat",
    },
  },
  defaultVariants: { size: "md" },
})

function Stat({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="stat"
      className={cn("grid content-start gap-1", className)}
      {...props}
    />
  )
}

function StatLabel({ className, ...props }: React.ComponentProps<"p">) {
  return (
    <p
      data-slot="stat-label"
      className={cn("text-label font-normal text-muted-foreground", className)}
      {...props}
    />
  )
}

function StatValue({
  size,
  className,
  ...props
}: React.ComponentProps<"p"> & VariantProps<typeof statValueVariants>) {
  return (
    <p
      data-slot="stat-value"
      className={cn(statValueVariants({ size, className }))}
      {...props}
    />
  )
}

// Write the sign and the period ("+2,140 this month"): the color only
// supports a change the words already state.
function StatDelta({
  trend = "neutral",
  className,
  ...props
}: React.ComponentProps<"p"> & { trend?: "up" | "down" | "neutral" }) {
  return (
    <p
      data-slot="stat-delta"
      data-trend={trend}
      className={cn(
        "text-caption",
        trend === "up" && "text-green-600 dark:text-green-400",
        trend === "down" && "text-destructive",
        trend === "neutral" && "text-muted-foreground",
        className
      )}
      {...props}
    />
  )
}

export { Stat, StatDelta, StatLabel, StatValue, statValueVariants }