Skip to contentVibraUI
Metrics

Big number

A headline metric that counts up on mount, with an optional label, affixes, and delta.

A client component — it animates. Readers who ask for less motion — with the OS setting, or with [data-motion="reduced"] on anything above the number, the kit's own switch — get the final value immediately, as does animate={false}. The count-up replays from 0 whenever value changes, so animate={false} suits live-updating numbers.

Install

npx shadcn@latest add @vibra/big-number

Needs the @vibra registry in your components.json — set it up once.

Examples

Props

PropTypeDefaultDescription
valuenumber—The number to display, and to count up to.
format(n: number) => stringwhole units with separatorsReceives the animating value every frame, so round inside it.
labelReact.ReactNode—Sits above the number.
deltanumber—Change since the previous period, rendered below as a MetricDelta.
deltaFormat"percent" | "number" | "compact""percent"How to format delta.
positiveIsGoodbooleantrueFalse for metrics where down is the win — churn, latency, cost.
animatebooleantrueCounts up from 0 on mount.
prefixReact.ReactNode—Set smaller and muted before the number, e.g. "$".
suffixReact.ReactNode—Set smaller and muted after the number, e.g. "MRR".
align"start" | "center""start"Centers the label, number, and delta as a column.

Dependencies

Source

components/ui/big-number.tsx
"use client"

import * as React from "react"

import { cn } from "@/lib/utils"
import { useCountUp } from "@/hooks/use-count-up"
import { formatNumber } from "@/lib/format"
import { MetricDelta, type MetricDeltaProps } from "@/components/ui/metric-delta"

/** Rounds to whole units — the count-up passes fractional values through every frame. */
function formatWhole(value: number) {
  return formatNumber(value, { maximumFractionDigits: 0 })
}

export type BigNumberProps = React.ComponentProps<"div"> & {
  value: number
  /** Receives the animating value, so round inside it. Defaults to whole units with separators. */
  format?: (n: number) => string
  label?: 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
  /** Counts up on mount. Under the OS setting or `[data-motion="reduced"]` the final value shows at once. */
  animate?: boolean
  prefix?: React.ReactNode
  suffix?: React.ReactNode
  align?: "start" | "center"
}

function BigNumber({
  className,
  value,
  format = formatWhole,
  label,
  delta,
  deltaFormat,
  positiveIsGood,
  animate = true,
  prefix,
  suffix,
  align = "start",
  ...props
}: BigNumberProps) {
  // The count reads the kit's motion switch where the number sits, so a
  // [data-motion="reduced"] on a frame's own wrapper stops it too.
  const numeral = React.useRef<HTMLDivElement | null>(null)
  const displayed = useCountUp(value, { enabled: animate, element: numeral })

  return (
    <div
      data-slot="big-number"
      data-align={align}
      className={cn(
        "flex flex-col gap-2",
        align === "center" && "items-center text-center",
        className
      )}
      {...props}
    >
      {label ? (
        <div data-slot="big-number-label" className="type-label text-muted-foreground">
          {label}
        </div>
      ) : null}

      {/* The numeral register at its largest: 40/44 on -0.02em. The tracking is
          the register's, not a tighter one invented here — a 40px number and a
          28px one on different tracking read as two typefaces. */}
      <div
        ref={numeral}
        data-slot="big-number-value"
        className="type-numeral flex items-baseline gap-1.5 text-4xl md:text-5xl"
      >
        {prefix ? (
          <span
            data-slot="big-number-prefix"
            className="text-2xl font-medium text-muted-foreground md:text-3xl"
          >
            {prefix}
          </span>
        ) : null}
        {format(displayed)}
        {suffix ? (
          <span
            data-slot="big-number-suffix"
            className="text-2xl font-medium text-muted-foreground md:text-3xl"
          >
            {suffix}
          </span>
        ) : null}
      </div>

      {delta !== undefined ? (
        <div data-slot="big-number-meta" className="font-sans tracking-normal">
          <MetricDelta value={delta} format={deltaFormat} positiveIsGood={positiveIsGood} />
        </div>
      ) : null}
    </div>
  )
}

export { BigNumber }