Skip to content
Facade UI
Colour mode
GitHub (opens in a new tab)
uiatom

Stat

One figure and its label as a description-list group, with a spoken form for abbreviated values.

Preview

Open full width (opens in a new tab)
Preview width

Installation

Package manager
pnpm dlx shadcn@latest add https://facadeui.dev/r/stat.json

Pulls in utils. The CLI installs them for you.

Usage

The exact source of the preview above.

demos/stat.tsx
import { Stat } from "@registry/ui/stat"

export function Demo() {
  return (
    <dl className="grid gap-8 sm:grid-cols-3">
      <Stat value="99.98%" label="Uptime" description="Rolling 90 days" />
      <Stat value="1.2K" srValue="1200" label="Teams onboarded" />
      <Stat value="18ms" label="Median response" />
    </dl>
  )
}

Source

What the CLI copies into your project, byte for byte.

components/ui/stat.tsx
/**
 * Stat — one figure and its label, as a description-list group.
 *
 * DOM order is `<dt>` (label) then `<dd>` (value) because that is the only order
 * a definition list permits; `flex-col-reverse` puts the number on top visually.
 * Reading order and visual order therefore differ by one line, which is the
 * right trade: the value is meaningless to a screen reader without its label
 * arriving first.
 *
 * Must be rendered inside a `<dl>` — the `Stats` section does that for you.
 *
 * `as` exists because of a constraint a `<dl>` actually enforces: it may contain
 * `dt`/`dd` pairs wrapped in at most **one** level of `div`. Wrapping `Stat` in
 * another element to style or animate it puts the pair two levels deep, which
 * is invalid and which axe flags as `definition-list` plus `dlitem`. So the
 * caller replaces this wrapper instead of adding one around it.
 *
 * a11y: `value` should include its own unit ("99.9%", "$2.4M"). Pass `srValue`
 * when the display form is an abbreviation a screen reader would mangle.
 *
 * Dependencies: react, @/lib/utils.
 */

import type { ElementType, ReactNode } from "react"

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

export interface StatItem {
  /** The figure, including its unit or symbol. */
  value: string
  label: string
  /** Optional sentence of context below the label. */
  description?: string
  /** Spoken form, when `value` is an abbreviation ("1.2K" -> "1200"). */
  srValue?: string
}

export interface StatProps extends StatItem {
  /** The group wrapper. Defaults to `"div"`, the only element a `<dl>` allows. */
  as?: ElementType
  size?: "sm" | "md" | "lg"
  align?: "start" | "center"
  className?: string
  children?: ReactNode
}

const valueSizes = {
  sm: "text-3xl",
  md: "text-display-sm",
  lg: "text-display-md",
} as const

export function Stat({
  value,
  label,
  description,
  srValue,
  as,
  size = "md",
  align = "start",
  className,
  children,
}: StatProps) {
  const Root = (as ?? "div") as ElementType

  return (
    <Root
      className={cn(
        "flex flex-col-reverse gap-2",
        align === "center" && "items-center text-center",
        className,
      )}
    >
      <dt className="text-muted-foreground text-sm font-medium">
        {label}
        {description ? (
          <span className="text-muted-foreground mt-1 block text-pretty text-sm font-normal">
            {description}
          </span>
        ) : null}
      </dt>
      <dd
        className={cn("text-foreground font-semibold tracking-tight", valueSizes[size])}
      >
        {srValue ? (
          <>
            <span aria-hidden>{value}</span>
            <span className="sr-only">{srValue}</span>
          </>
        ) : (
          value
        )}
        {children}
      </dd>
    </Root>
  )
}

Props

StatProps

Extends StatItem.

Props for StatProps
PropTypeDefault
asThe group wrapper. Defaults to `"div"`, the only element a `<dl>` allows.ElementType
size"sm" | "md" | "lg""md"
align"start" | "center""start"
classNamestring
childrenReactNode