data displayclient

stat

a big number that counts up into place, with a label and a trend.

0↑ 12%
total reads
0↑ 4%
subscribers
0
articles
0★
avg. rating

installation

new to blurrow? this sets up your project in one go: the package, the styles and the font. already done it? skip straight to usage. the installation page has the details.

bash
npx @blurrow/ui init
›rather own the code? copy the source instead

let the cli copy it, along with anything it needs:

bash
npx @blurrow/ui add stat

or by hand: drop this file into components/ui and import from there.

components/ui/Stat.tsxtsx
"use client";
import React, { useEffect, useRef, useState } from "react";
interface StatProps {
value: number | string;
label: string;
prefix?: string;
suffix?: string;
trend?: string;
className?: string;
}
function useCountUp(target: number, duration = 800) {
const [count, setCount] = useState(0);
const raf = useRef<number | null>(null);
useEffect(() => {
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
raf.current = requestAnimationFrame(() => setCount(target));
return () => { if (raf.current) cancelAnimationFrame(raf.current); };
}
let start: number | null = null;
const step = (now: number) => {
if (!start) start = now;
const p = Math.min((now - start) / duration, 1);
setCount(Math.round((1 - Math.pow(1 - p, 3)) * target));
if (p < 1) raf.current = requestAnimationFrame(step);
};
raf.current = requestAnimationFrame(step);
return () => { if (raf.current) cancelAnimationFrame(raf.current); };
}, [target, duration]);
return count;
}
export function Stat({ value, label, prefix = "", suffix = "", trend, className = "" }: StatProps) {
const isNum = typeof value === "number";
const counted = useCountUp(isNum ? (value as number) : 0);
const displayed = isNum ? counted : value;
return (
<div className={["flex flex-col gap-0.5", className].join(" ")}>
<div className="flex items-baseline gap-1.5">
<span
className="text-[32px] font-semibold text-neutral-900 leading-none tabular-nums"
style={{ letterSpacing: "-0.03em" }}
>
{prefix}{displayed}{suffix}
</span>
{trend && (
<span className="text-[12px] text-neutral-500 font-medium">{trend}</span>
)}
</div>
<span className="text-[12px] text-neutral-500">{label}</span>
</div>
);
}
interface StatRowProps {
children: React.ReactNode;
className?: string;
}
export function StatRow({ children, className = "" }: StatRowProps) {
const arr = React.Children.toArray(children);
return (
<div className={["grid grid-cols-2 gap-6 sm:flex sm:divide-x sm:divide-neutral-200 sm:gap-0", className].join(" ")}>
{arr.map((child, i) => (
<div
key={i}
className={[
"min-w-0",
i === 0 ? "sm:pr-6" : i === arr.length - 1 ? "sm:pl-6" : "sm:px-6",
].join(" ")}
>
{child}
</div>
))}
</div>
);
}

usage

tsx
import { Stat, StatRow } from "@blurrow/ui";
tsx
<StatRow>
<Stat value={2847} label="total reads" trend="↑ 12%" />
</StatRow>

props

straight from the source, so they're always up to date.

propsts
interface StatProps {
value: number | string;
label: string;
prefix?: string;
suffix?: string;
trend?: string;
className?: string;
}
interface StatRowProps {
children: React.ReactNode;
className?: string;
}