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-numberNeeds the @vibra registry in your components.json — set it up once.
Examples
import { BigNumber } from "@/components/ui/big-number"
export default function BigNumberDemo() {
return (
<div className="w-full max-w-sm panel p-6">
<BigNumber label="Annual recurring revenue" value={2480000} prefix="$" delta={0.083} />
</div>
)
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
| value | number | — | The number to display, and to count up to. |
| format | (n: number) => string | whole units with separators | Receives the animating value every frame, so round inside it. |
| label | React.ReactNode | — | Sits above the number. |
| delta | number | — | Change since the previous period, rendered below as a MetricDelta. |
| deltaFormat | "percent" | "number" | "compact" | "percent" | How to format delta. |
| positiveIsGood | boolean | true | False for metrics where down is the win — churn, latency, cost. |
| animate | boolean | true | Counts up from 0 on mount. |
| prefix | React.ReactNode | — | Set smaller and muted before the number, e.g. "$". |
| suffix | React.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
"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 }