Stats

Metric layouts that put the important numbers first.

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.

  • 200+

    Blocks

  • 60

    Components

  • 12

    Themes

  • 99.9%

    Uptime

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-01

Usage

import { Stats01 } from "@/components/blocks/stats/stats-01"
<Stats01 />

Number ticker

Odometer-style digits that roll into place as the section enters view.

  • +

    Pages shipped

  • %

    Satisfaction

  • Components

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-02

Usage

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-03

Usage

import { Stats03 } from "@/components/blocks/stats/stats-03"
<Stats03 />