Clean ways to surface a few numbers without overexplaining them. Replace the values and labels with your own.
Metric strip
Four key numbers in a centered row, stacking two by two on narrow widths.
tsx
"use client";import { cn } from "@/lib/utils";type Stats01Props = { className?: string;};const STATS = [ { value: "200+", label: "Blocks" }, { value: "60", label: "Components" }, { value: "12", label: "Themes" }, { value: "99.9%", label: "Uptime" },] as const;export function Stats01({ className }: Stats01Props) { return ( <section className={cn("@container w-full bg-background", className)}> <div className="mx-auto w-full max-w-4xl px-4 py-10 @[32rem]:px-6 @[32rem]:py-14"> <ul className="grid grid-cols-2 gap-y-8 @[32rem]:grid-cols-4 @[32rem]:gap-0 @[32rem]:divide-x @[32rem]:divide-border"> {STATS.map((stat) => ( <li key={stat.label} className="flex flex-col items-center text-center" > <p className="text-3xl font-medium tracking-tight text-foreground @[32rem]:text-4xl"> {stat.value} </p> <p className="mt-1.5 text-xs tracking-wide text-muted-foreground"> {stat.label} </p> </li> ))} </ul> </div> </section> );}Installation
npx @arctis-sh/@arctis-sh/ui@latest add stats-01Usage
import { Stats01 } from "@/components/blocks/stats/stats-01"<Stats01 />Number ticker
Odometer-style digits that roll into place as the section enters view.
tsx
"use client";import { useEffect, useRef, useState } from "react";import { cn } from "@/lib/utils";type Stats02Props = { className?: string;};const STATS = [ { value: "2400", suffix: "+", label: "Pages shipped" }, { value: "99", suffix: "%", label: "Satisfaction" }, { value: "60", suffix: "", label: "Components" },] as const;function TickerDigit({ digit, active, delay,}: { digit: string; active: boolean; delay: number;}) { const n = Number(digit); if (Number.isNaN(n)) { return <span className="inline-block px-0.5">{digit}</span>; } return ( <span className="relative inline-block h-[1em] w-[0.62em] overflow-hidden"> <span aria-hidden className="absolute inset-x-0 top-0 flex flex-col will-change-transform" style={{ transform: active ? `translateY(${-n}em)` : "translateY(0)", transition: active ? `transform 900ms cubic-bezier(0.22, 1, 0.36, 1) ${delay}ms` : "none", }} > {Array.from({ length: 10 }, (_, i) => ( <span key={i} className="flex h-[1em] items-center justify-center tabular-nums" > {i} </span> ))} </span> <span className="invisible tabular-nums">{digit}</span> </span> );}function TickerValue({ value, suffix, active, stagger,}: { value: string; suffix: string; active: boolean; stagger: number;}) { return ( <p className="flex items-baseline text-4xl font-medium tracking-tight text-foreground @[32rem]:text-5xl"> <span className="inline-flex" aria-label={`${value}${suffix}`}> {value.split("").map((digit, index) => ( <TickerDigit key={`${digit}-${index}`} digit={digit} active={active} delay={stagger + index * 70} /> ))} </span> {suffix ? ( <span className="ml-0.5 text-[0.7em] tracking-tight">{suffix}</span> ) : null} </p> );}export function Stats02({ className }: Stats02Props) { const rootRef = useRef<HTMLElement>(null); const [active, setActive] = useState(false); useEffect(() => { const el = rootRef.current; if (!el) return; const observer = new IntersectionObserver( ([entry]) => { if (entry?.isIntersecting) { setActive(true); observer.disconnect(); } }, { threshold: 0.35 }, ); observer.observe(el); return () => observer.disconnect(); }, []); return ( <section ref={rootRef} className={cn("@container w-full bg-background", className)} > <div className="mx-auto w-full max-w-4xl px-4 py-10 @[32rem]:px-6 @[32rem]:py-14"> <ul className="grid grid-cols-1 gap-8 @[32rem]:grid-cols-3 @[32rem]:gap-0 @[32rem]:divide-x @[32rem]:divide-border"> {STATS.map((stat, index) => ( <li key={stat.label} className="flex flex-col items-center text-center" > <TickerValue value={stat.value} suffix={stat.suffix} active={active} stagger={index * 120} /> <p className="mt-2 text-xs tracking-wide text-muted-foreground"> {stat.label} </p> </li> ))} </ul> </div> </section> );}Installation
npx @arctis-sh/@arctis-sh/ui@latest add stats-02Usage
import { Stats02 } from "@/components/blocks/stats/stats-02"<Stats02 />Grid
Four metrics in a bordered grid, each paired with a short explanation.
12k+
Pages shipped
Teams reuse the same blocks across marketing and product.
4.9
Average rating
Clean defaults that still feel custom once themed.
30s
Install time
Add a block with the CLI and wire it into a route.
0
Design debt
One token system for light, dark, and everything between.
tsx
"use client";import { cn } from "@/lib/utils";type Stats03Props = { className?: string;};const STATS = [ { value: "12k+", label: "Pages shipped", description: "Teams reuse the same blocks across marketing and product.", }, { value: "4.9", label: "Average rating", description: "Clean defaults that still feel custom once themed.", }, { value: "30s", label: "Install time", description: "Add a block with the CLI and wire it into a route.", }, { value: "0", label: "Design debt", description: "One token system for light, dark, and everything between.", },] as const;export function Stats03({ className }: Stats03Props) { return ( <section className={cn("@container w-full bg-background", className)}> <div className="mx-auto w-full max-w-4xl px-4 py-10 @[32rem]:px-6 @[32rem]:py-14"> <ul className="grid grid-cols-1 gap-px overflow-hidden rounded-xl border border-border bg-border @[32rem]:grid-cols-2"> {STATS.map((stat) => ( <li key={stat.label} className="bg-background px-5 py-6 @[32rem]:px-6 @[32rem]:py-8" > <p className="text-3xl font-medium tracking-tight text-foreground"> {stat.value} </p> <p className="mt-2 text-sm font-medium tracking-wide text-foreground"> {stat.label} </p> <p className="mt-1.5 max-w-xs text-sm tracking-wide text-muted-foreground"> {stat.description} </p> </li> ))} </ul> </div> </section> );}Installation
npx @arctis-sh/@arctis-sh/ui@latest add stats-03Usage
import { Stats03 } from "@/components/blocks/stats/stats-03"<Stats03 />