Stats strip
Four big numbers that count up once in view, with mono labels and hairline dividers.
blockstatsany
$
npx shadcn@latest add @storekit/trust-statstrust-stats
Live and interactive. Switch the brand in the frame header to see the same source under another token file.
Installation
$
npx shadcn@latest add @storekit/trust-stats$
pnpm dlx shadcn@latest add @storekit/trust-stats$
yarn dlx shadcn@latest add @storekit/trust-stats$
bunx --bun shadcn@latest add @storekit/trust-statsSource
components/storekit/trust/trust-stats.tsx
89 lines
"use client"
import { useEffect, useRef } from "react"
import { animate, useInView, useReducedMotion } from "motion/react"
import { cn } from "@/components/storekit/lib/cn"
/* Four-up proof strip. Numbers count up from zero once, the first time the
strip scrolls into view. Tabular numerals keep the width steady while they
count; reduced motion shows the final value immediately. */
const EASE = [0.23, 1, 0.32, 1] as const
type Stat = { value: number; label: string; decimals?: number; prefix?: string; suffix?: string }
function format(n: number, s: Stat) {
const num = n.toLocaleString("en-US", { minimumFractionDigits: s.decimals ?? 0, maximumFractionDigits: s.decimals ?? 0 })
return `${s.prefix ?? ""}${num}${s.suffix ?? ""}`
}
function Counter({ stat, run }: { stat: Stat; run: boolean }) {
const ref = useRef<HTMLSpanElement>(null)
const reduce = useReducedMotion()
useEffect(() => {
const node = ref.current
if (!node || !run) return
if (reduce) {
node.textContent = format(stat.value, stat)
return
}
const controls = animate(0, stat.value, {
duration: 1.4,
ease: EASE,
onUpdate: (v) => {
node.textContent = format(v, stat)
},
})
return () => controls.stop()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [run, reduce, stat.value, stat.decimals, stat.prefix, stat.suffix])
return (
<span ref={ref} aria-hidden>
{format(0, stat)}
</span>
)
}
export function TrustStats({
stats = [
{ value: 1240, label: "Batches tested", suffix: "+" },
{ value: 120, label: "Members on subscription", suffix: "k" },
{ value: 48, label: "Hours to ship", suffix: "h" },
{ value: 4.9, label: "Customer rating", decimals: 1 },
],
className,
}: {
stats?: Stat[]
className?: string
}) {
const ref = useRef<HTMLDListElement>(null)
const inView = useInView(ref, { once: true, margin: "0px 0px -15% 0px" })
return (
<dl
ref={ref}
data-sk-section="trust"
data-sk-variant="trust-stats"
className={cn(
"grid grid-cols-2 gap-px overflow-hidden rounded-sk-card border border-sk-line bg-sk-line lg:grid-cols-4",
className
)}
>
{stats.map((s) => (
<div key={s.label} className="flex flex-col-reverse justify-end gap-2 bg-sk-paper px-5 py-6 sm:px-6 sm:py-8">
<dt className="font-sk-mono text-[11px] uppercase tracking-[0.1em] text-sk-muted sm:tracking-[0.14em]">{s.label}</dt>
<dd
className="font-sk-display font-semibold leading-[1.1] tracking-[-0.02em] tabular-nums text-sk-ink"
style={{ fontSize: "clamp(2rem, 4vw, 3rem)" }}
>
<span className="sr-only">{format(s.value, s)}</span>
<Counter stat={s} run={inView} />
</dd>
</div>
))}
</dl>
)
}
Usage
Import from "@/components/storekit/trust/trust-stats" and pass your product data. The demo above exercises the full prop surface with sample values.