Sections

Stats Band

Four-stat band whose values count up once in view, with hairline separators and an optional inverted dark variant.

Install

npx shadcn@latest add @paragon/stats-band

Also installs: number-ticker

stats-band.tsx

"use client";

import * as React from "react";
import { useInView, useReducedMotion } from "motion/react";
import { cn } from "@/lib/utils";
import { NumberTicker } from "@/registry/paragon/ui/number-ticker";

const REVEAL =
  "transition-[opacity,translate,filter] duration-500 ease-out motion-reduce:transition-none";
const HIDDEN = "translate-y-3 opacity-0 blur-[4px]";
const SHOWN = "translate-y-0 opacity-100 blur-[0px]";

const STATS: {
  value: number;
  suffix?: string;
  label: string;
}[] = [
  { value: 99.99, suffix: "%", label: "Uptime over the last 12 months" },
  { value: 2.4, suffix: "B", label: "Events processed every day" },
  { value: 87, suffix: "ms", label: "Median query latency" },
  { value: 3200, suffix: "+", label: "Engineering teams on the platform" },
];

export interface StatsBandProps extends React.ComponentProps<"section"> {
  /**
   * Renders the band on the dark surface regardless of page theme —
   * an inverted panel for light marketing pages.
   */
  inverted?: boolean;
}

/**
 * Four-stat band: values count up once on first view (tabular-nums, so
 * nothing shifts mid-count), labels beneath, hairline separators between
 * columns. `inverted` flips the band to the dark surface.
 */
export function StatsBand({
  inverted = false,
  className,
  ...props
}: StatsBandProps) {
  const ref = React.useRef<HTMLElement>(null);
  const inView = useInView(ref, { once: true, margin: "-80px" });
  const reduced = useReducedMotion();
  const revealed = inView || !!reduced;

  return (
    <section
      ref={ref}
      aria-label="Platform statistics"
      className={cn(
        "w-full bg-background px-4 py-14 text-foreground sm:px-6 md:py-20",
        inverted && "dark",
        className,
      )}
      {...props}
    >
      <div className="mx-auto w-full max-w-5xl">
        <dl className="grid grid-cols-2 gap-x-6 gap-y-10 lg:grid-cols-4 lg:gap-x-0 lg:divide-x lg:divide-border">
          {STATS.map((stat, i) => (
            <div
              key={stat.label}
              className={cn(
                "flex flex-col lg:px-8 lg:first:pl-0 lg:last:pr-0",
                REVEAL,
                revealed ? SHOWN : HIDDEN,
              )}
              style={{ transitionDelay: `${i * 80}ms` }}
            >
              <dd className="order-1 text-4xl font-semibold tracking-tight tabular-nums">
                <NumberTicker
                  value={stat.value}
                  duration={0.9}
                  delay={0.15 + i * 0.08}
                />
                {stat.suffix && (
                  <span className="text-muted-foreground">{stat.suffix}</span>
                )}
              </dd>
              <dt className="order-2 mt-2 text-sm text-pretty text-muted-foreground">
                {stat.label}
              </dt>
            </div>
          ))}
        </dl>
      </div>
    </section>
  );
}