Web InnoventixFreeCode

Stats Hero

Original · free

hero with a stats strip

byWeb InnoventixReact + Tailwind
heroxstatsheroes
Open

Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.

npx shadcn@latest add https://webinnoventix.com/r/herox-stats.json
herox-stats.tsx
"use client";

import { useEffect, useId, useRef, useState, type KeyboardEvent } from "react";
import { motion, useReducedMotion, type Variants } from "motion/react";

type Arrow = "up" | "down" | "none";
type Tone = "pos" | "neg" | "neutral";

type StatItem = {
  label: string;
  value: number;
  decimals: number;
  prefix: string;
  suffix: string;
  delta: string;
  arrow: Arrow;
  tone: Tone;
};

type StatGroup = {
  id: string;
  label: string;
  stats: StatItem[];
};

const GROUPS: StatGroup[] = [
  {
    id: "overview",
    label: "Overview",
    stats: [
      {
        label: "Teams onboarded",
        value: 4200,
        decimals: 0,
        prefix: "",
        suffix: "+",
        delta: "+18% this quarter",
        arrow: "up",
        tone: "pos",
      },
      {
        label: "Uptime, last 12 months",
        value: 99.98,
        decimals: 2,
        prefix: "",
        suffix: "%",
        delta: "SLA-backed",
        arrow: "none",
        tone: "neutral",
      },
      {
        label: "Revenue tracked",
        value: 2.4,
        decimals: 1,
        prefix: "$",
        suffix: "B",
        delta: "+31% year over year",
        arrow: "up",
        tone: "pos",
      },
      {
        label: "Avg. time to first insight",
        value: 38,
        decimals: 0,
        prefix: "",
        suffix: " min",
        delta: "-52% vs 2024",
        arrow: "down",
        tone: "pos",
      },
    ],
  },
  {
    id: "growth",
    label: "Growth",
    stats: [
      {
        label: "Pipeline velocity",
        value: 3.1,
        decimals: 1,
        prefix: "",
        suffix: "x",
        delta: "+0.6x this quarter",
        arrow: "up",
        tone: "pos",
      },
      {
        label: "Activation rate",
        value: 42,
        decimals: 0,
        prefix: "+",
        suffix: "%",
        delta: "+9 points",
        arrow: "up",
        tone: "pos",
      },
      {
        label: "Workflows automated",
        value: 12800,
        decimals: 0,
        prefix: "",
        suffix: "",
        delta: "+2,100 this quarter",
        arrow: "up",
        tone: "pos",
      },
      {
        label: "Native integrations",
        value: 27,
        decimals: 0,
        prefix: "",
        suffix: "",
        delta: "6 added in 2025",
        arrow: "up",
        tone: "neutral",
      },
    ],
  },
  {
    id: "reliability",
    label: "Reliability",
    stats: [
      {
        label: "Uptime SLA",
        value: 99.98,
        decimals: 2,
        prefix: "",
        suffix: "%",
        delta: "Guaranteed",
        arrow: "none",
        tone: "neutral",
      },
      {
        label: "Median query time",
        value: 118,
        decimals: 0,
        prefix: "",
        suffix: " ms",
        delta: "-24 ms this quarter",
        arrow: "down",
        tone: "pos",
      },
      {
        label: "Support coverage",
        value: 24,
        decimals: 0,
        prefix: "",
        suffix: "/7",
        delta: "3 min median reply",
        arrow: "none",
        tone: "neutral",
      },
      {
        label: "Data incidents, 2025",
        value: 0,
        decimals: 0,
        prefix: "",
        suffix: "",
        delta: "3 years clean",
        arrow: "none",
        tone: "pos",
      },
    ],
  },
];

function formatNumber(n: number, decimals: number): string {
  return n.toLocaleString("en-US", {
    minimumFractionDigits: decimals,
    maximumFractionDigits: decimals,
  });
}

function useCountUp(value: number, enabled: boolean, duration = 1200): number {
  const [display, setDisplay] = useState<number>(enabled ? 0 : value);

  useEffect(() => {
    if (!enabled) {
      setDisplay(value);
      return;
    }
    let raf = 0;
    let start = 0;
    const step = (t: number) => {
      if (start === 0) start = t;
      const progress = Math.min((t - start) / duration, 1);
      const eased = 1 - Math.pow(1 - progress, 3);
      setDisplay(value * eased);
      if (progress < 1) raf = requestAnimationFrame(step);
      else setDisplay(value);
    };
    raf = requestAnimationFrame(step);
    return () => cancelAnimationFrame(raf);
  }, [value, enabled, duration]);

  return display;
}

function TrendIcon({ arrow }: { arrow: Arrow }) {
  if (arrow === "none") {
    return (
      <svg viewBox="0 0 16 16" aria-hidden="true" className="h-3 w-3">
        <path
          d="M3 8h10"
          fill="none"
          stroke="currentColor"
          strokeWidth="2"
          strokeLinecap="round"
        />
      </svg>
    );
  }
  const isUp = arrow === "up";
  return (
    <svg viewBox="0 0 16 16" aria-hidden="true" className="h-3 w-3">
      <path
        d={isUp ? "M8 13V3M8 3l-4 4M8 3l4 4" : "M8 3v10M8 13l-4-4M8 13l4-4"}
        fill="none"
        stroke="currentColor"
        strokeWidth="2"
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </svg>
  );
}

function StatCard({
  stat,
  animate,
  index,
}: {
  stat: StatItem;
  animate: boolean;
  index: number;
}) {
  const display = useCountUp(stat.value, animate);
  const shown = animate ? display : stat.value;

  const toneText =
    stat.tone === "pos"
      ? "text-emerald-600 dark:text-emerald-400"
      : stat.tone === "neg"
        ? "text-rose-600 dark:text-rose-400"
        : "text-slate-500 dark:text-slate-400";

  const tonePill =
    stat.tone === "pos"
      ? "bg-emerald-500/10 text-emerald-700 ring-emerald-500/20 dark:text-emerald-300"
      : stat.tone === "neg"
        ? "bg-rose-500/10 text-rose-700 ring-rose-500/20 dark:text-rose-300"
        : "bg-slate-500/10 text-slate-600 ring-slate-500/20 dark:text-slate-300";

  return (
    <div
      className="hxs-fade group relative flex flex-col gap-3 rounded-2xl border border-slate-200/70 bg-white/60 p-5 backdrop-blur-sm transition-colors hover:border-indigo-300 dark:border-white/10 dark:bg-white/[0.03] dark:hover:border-indigo-400/40"
      style={{ animationDelay: `${index * 70}ms` }}
    >
      <div className="flex items-baseline gap-1 font-semibold tracking-tight text-slate-900 tabular-nums dark:text-white">
        <span className="text-3xl sm:text-4xl">
          {stat.prefix}
          {formatNumber(shown, stat.decimals)}
          {stat.suffix}
        </span>
      </div>
      <p className="text-sm font-medium text-slate-600 dark:text-slate-300">
        {stat.label}
      </p>
      <span
        className={`inline-flex w-fit items-center gap-1 rounded-full px-2 py-0.5 text-xs font-semibold ring-1 ${tonePill}`}
      >
        <span className={toneText}>
          <TrendIcon arrow={stat.arrow} />
        </span>
        {stat.delta}
      </span>
    </div>
  );
}

export default function HeroxStats() {
  const reduce = useReducedMotion();
  const uid = useId();
  const [activeId, setActiveId] = useState<string>(GROUPS[0].id);
  const [inView, setInView] = useState<boolean>(false);
  const tabRefs = useRef<Array<HTMLButtonElement | null>>([]);

  const activeIndex = GROUPS.findIndex((g) => g.id === activeId);
  const activeGroup = GROUPS[activeIndex] ?? GROUPS[0];
  const animateCount = inView && !reduce;

  const onTabKeyDown = (event: KeyboardEvent<HTMLButtonElement>) => {
    if (event.key !== "ArrowRight" && event.key !== "ArrowLeft") return;
    event.preventDefault();
    const dir = event.key === "ArrowRight" ? 1 : -1;
    const next = (activeIndex + dir + GROUPS.length) % GROUPS.length;
    setActiveId(GROUPS[next].id);
    tabRefs.current[next]?.focus();
  };

  const container: Variants = {
    hidden: {},
    show: {
      transition: { staggerChildren: 0.09, delayChildren: 0.05 },
    },
  };

  const item: Variants = reduce
    ? { hidden: { opacity: 1, y: 0 }, show: { opacity: 1, y: 0 } }
    : {
        hidden: { opacity: 0, y: 20 },
        show: {
          opacity: 1,
          y: 0,
          transition: { duration: 0.6, ease: "easeOut" },
        },
      };

  return (
    <section className="relative w-full overflow-hidden bg-gradient-to-b from-slate-50 via-white to-slate-100 px-6 py-24 sm:py-28 lg:py-32 dark:from-slate-950 dark:via-slate-950 dark:to-slate-900">
      <style>{`
        @keyframes hxs-floaty {
          0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
          50% { transform: translate3d(0, -18px, 0) scale(1.05); }
        }
        @keyframes hxs-drift {
          0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
          50% { transform: translate3d(14px, 12px, 0) scale(1.08); }
        }
        @keyframes hxs-pulse {
          0%, 100% { opacity: 1; transform: scale(1); }
          50% { opacity: 0.35; transform: scale(0.6); }
        }
        @keyframes hxs-dash {
          to { stroke-dashoffset: 0; }
        }
        @keyframes hxs-rise {
          from { opacity: 0; transform: translateY(14px); }
          to { opacity: 1; transform: translateY(0); }
        }
        .hxs-fade { animation: hxs-rise 0.6s cubic-bezier(0.16,1,0.3,1) both; }
        @media (prefers-reduced-motion: reduce) {
          .hxs-blob-a, .hxs-blob-b, .hxs-live-dot, .hxs-spark, .hxs-fade {
            animation: none !important;
          }
          .hxs-spark { stroke-dashoffset: 0 !important; }
        }
      `}</style>

      {/* Decorative background */}
      <div aria-hidden="true" className="pointer-events-none absolute inset-0">
        <div
          className="hxs-blob-a absolute -left-24 top-8 h-72 w-72 rounded-full bg-indigo-400/25 blur-3xl dark:bg-indigo-500/20"
          style={{ animation: "hxs-floaty 11s ease-in-out infinite" }}
        />
        <div
          className="hxs-blob-b absolute -right-16 top-32 h-80 w-80 rounded-full bg-violet-400/20 blur-3xl dark:bg-violet-500/20"
          style={{ animation: "hxs-drift 13s ease-in-out infinite" }}
        />
        <div
          className="absolute inset-0 opacity-[0.4] dark:opacity-[0.25]"
          style={{
            backgroundImage:
              "linear-gradient(to right, rgb(100 116 139 / 0.12) 1px, transparent 1px), linear-gradient(to bottom, rgb(100 116 139 / 0.12) 1px, transparent 1px)",
            backgroundSize: "56px 56px",
            maskImage:
              "radial-gradient(ellipse 70% 60% at 50% 0%, black 40%, transparent 100%)",
            WebkitMaskImage:
              "radial-gradient(ellipse 70% 60% at 50% 0%, black 40%, transparent 100%)",
          }}
        />
      </div>

      <div className="relative mx-auto max-w-6xl">
        {/* Hero copy */}
        <motion.div
          variants={container}
          initial="hidden"
          whileInView="show"
          viewport={{ once: true, margin: "-80px" }}
          className="mx-auto max-w-3xl text-center"
        >
          <motion.span
            variants={item}
            className="inline-flex items-center gap-2 rounded-full border border-indigo-200 bg-indigo-50/80 px-3.5 py-1.5 text-xs font-semibold uppercase tracking-wider text-indigo-700 dark:border-indigo-400/20 dark:bg-indigo-400/10 dark:text-indigo-300"
          >
            <span className="relative flex h-1.5 w-1.5">
              <span
                className="hxs-live-dot absolute inline-flex h-full w-full rounded-full bg-indigo-500"
                style={{ animation: "hxs-pulse 2s ease-in-out infinite" }}
              />
              <span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-indigo-500" />
            </span>
            Revenue intelligence, unified
          </motion.span>

          <motion.h1
            variants={item}
            className="mt-6 text-balance text-4xl font-semibold tracking-tight text-slate-900 sm:text-5xl lg:text-6xl dark:text-white"
          >
            Turn scattered signals into decisions your team{" "}
            <span className="bg-gradient-to-r from-indigo-600 via-violet-600 to-sky-600 bg-clip-text text-transparent dark:from-indigo-400 dark:via-violet-400 dark:to-sky-400">
              actually ships
            </span>
            .
          </motion.h1>

          <motion.p
            variants={item}
            className="mx-auto mt-6 max-w-2xl text-pretty text-lg leading-relaxed text-slate-600 dark:text-slate-300"
          >
            Cadence unifies your product, billing, and support data into one
            live workspace, so every team moves on the same numbers in real
            time. No exports, no stale dashboards, no arguing about whose spreadsheet is right.
          </motion.p>

          <motion.div
            variants={item}
            className="mt-9 flex flex-col items-center justify-center gap-3 sm:flex-row"
          >
            <button
              type="button"
              className="group inline-flex w-full items-center justify-center gap-2 rounded-xl bg-slate-900 px-6 py-3 text-sm font-semibold text-white shadow-lg shadow-slate-900/10 transition-all hover:-translate-y-0.5 hover:bg-indigo-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 sm:w-auto dark:bg-white dark:text-slate-900 dark:hover:bg-indigo-400 dark:hover:text-white"
            >
              Start free, no card
              <svg
                viewBox="0 0 20 20"
                aria-hidden="true"
                className="h-4 w-4 transition-transform group-hover:translate-x-0.5"
              >
                <path
                  d="M4 10h11M11 5l5 5-5 5"
                  fill="none"
                  stroke="currentColor"
                  strokeWidth="2"
                  strokeLinecap="round"
                  strokeLinejoin="round"
                />
              </svg>
            </button>
            <a
              href="#demo"
              className="inline-flex w-full items-center justify-center gap-2 rounded-xl border border-slate-300 bg-white/70 px-6 py-3 text-sm font-semibold text-slate-800 backdrop-blur transition-all hover:-translate-y-0.5 hover:border-slate-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 sm:w-auto dark:border-white/15 dark:bg-white/5 dark:text-slate-100 dark:hover:border-white/30"
            >
              <svg viewBox="0 0 20 20" aria-hidden="true" className="h-4 w-4">
                <path
                  d="M7 5.5v9l7-4.5-7-4.5Z"
                  fill="currentColor"
                />
              </svg>
              Book a 20-min demo
            </a>
          </motion.div>

          <motion.div
            variants={item}
            className="mt-8 flex flex-col items-center justify-center gap-3 text-sm text-slate-500 sm:flex-row dark:text-slate-400"
          >
            <div className="flex -space-x-2">
              {["from-indigo-400 to-violet-500", "from-sky-400 to-indigo-500", "from-emerald-400 to-sky-500", "from-rose-400 to-violet-500", "from-amber-400 to-rose-500"].map(
                (g, i) => (
                  <span
                    key={i}
                    className={`inline-block h-7 w-7 rounded-full bg-gradient-to-br ${g} ring-2 ring-white dark:ring-slate-950`}
                  />
                ),
              )}
            </div>
            <span>
              Trusted by{" "}
              <span className="font-semibold text-slate-700 dark:text-slate-200">
                4,200+ operators
              </span>{" "}
              <span className="mx-1 text-slate-300 dark:text-slate-600">|</span>{" "}
              SOC 2 Type II
            </span>
          </motion.div>
        </motion.div>

        {/* Stats strip */}
        <motion.div
          initial={reduce ? undefined : { opacity: 0, y: 28 }}
          whileInView={reduce ? undefined : { opacity: 1, y: 0 }}
          viewport={{ once: true, margin: "-60px" }}
          transition={{ duration: 0.7, ease: "easeOut", delay: 0.1 }}
          onViewportEnter={() => setInView(true)}
          className="relative mx-auto mt-16 max-w-5xl overflow-hidden rounded-3xl border border-slate-200/80 bg-white/70 p-4 shadow-xl shadow-slate-900/5 backdrop-blur-md sm:p-6 dark:border-white/10 dark:bg-slate-900/50 dark:shadow-black/20"
        >
          {/* Card sparkline decoration */}
          <svg
            aria-hidden="true"
            viewBox="0 0 600 120"
            preserveAspectRatio="none"
            className="pointer-events-none absolute inset-x-0 bottom-0 h-24 w-full opacity-40 dark:opacity-30"
          >
            <defs>
              <linearGradient id={`${uid}-spark`} x1="0" y1="0" x2="1" y2="0">
                <stop offset="0%" stopColor="rgb(99 102 241)" stopOpacity="0.1" />
                <stop offset="50%" stopColor="rgb(139 92 246)" stopOpacity="0.7" />
                <stop offset="100%" stopColor="rgb(56 189 248)" stopOpacity="0.1" />
              </linearGradient>
            </defs>
            <path
              className="hxs-spark"
              d="M0 90 C60 90 90 40 150 50 C210 60 240 20 300 30 C360 40 390 80 450 70 C510 60 540 30 600 45"
              fill="none"
              stroke={`url(#${uid}-spark)`}
              strokeWidth="2.5"
              strokeLinecap="round"
              strokeDasharray="900"
              strokeDashoffset="900"
              style={{
                animation: "hxs-dash 2.2s ease-out 0.4s forwards",
              }}
            />
          </svg>

          {/* Header: tabs + live badge */}
          <div className="relative mb-5 flex flex-col items-start justify-between gap-3 sm:flex-row sm:items-center">
            <div
              role="tablist"
              aria-label="Metric groups"
              className="inline-flex rounded-xl bg-slate-100 p-1 dark:bg-white/5"
            >
              {GROUPS.map((group, i) => {
                const selected = group.id === activeId;
                return (
                  <button
                    key={group.id}
                    ref={(el) => {
                      tabRefs.current[i] = el;
                    }}
                    role="tab"
                    id={`${uid}-tab-${group.id}`}
                    aria-selected={selected}
                    aria-controls={`${uid}-panel`}
                    tabIndex={selected ? 0 : -1}
                    onClick={() => setActiveId(group.id)}
                    onKeyDown={onTabKeyDown}
                    className={`relative rounded-lg px-3.5 py-1.5 text-sm font-semibold transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 sm:px-4 ${
                      selected
                        ? "bg-white text-slate-900 shadow-sm dark:bg-slate-800 dark:text-white"
                        : "text-slate-500 hover:text-slate-800 dark:text-slate-400 dark:hover:text-slate-200"
                    }`}
                  >
                    {group.label}
                  </button>
                );
              })}
            </div>

            <span className="inline-flex items-center gap-2 text-xs font-medium text-slate-500 dark:text-slate-400">
              <span className="relative flex h-2 w-2">
                <span
                  className="hxs-live-dot absolute inline-flex h-full w-full rounded-full bg-emerald-500"
                  style={{ animation: "hxs-pulse 1.8s ease-in-out infinite" }}
                />
                <span className="relative inline-flex h-2 w-2 rounded-full bg-emerald-500" />
              </span>
              Live, updated every 60s
            </span>
          </div>

          {/* Stats grid */}
          <div
            key={reduce ? "static" : activeId}
            role="tabpanel"
            id={`${uid}-panel`}
            aria-labelledby={`${uid}-tab-${activeGroup.id}`}
            className="relative grid grid-cols-2 gap-3 sm:gap-4 lg:grid-cols-4"
          >
            {activeGroup.stats.map((stat, i) => (
              <StatCard
                key={`${activeGroup.id}-${stat.label}`}
                stat={stat}
                animate={animateCount}
                index={i}
              />
            ))}
          </div>
        </motion.div>
      </div>
    </section>
  );
}

Dependencies

motion

Licence

Built by Web Innoventix. Free for personal and commercial use, no attribution required.

Built by Web Innoventix

Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.

Get a free quote

Similar components

Browse all →
Spotlight Hero

Spotlight Hero

Original

A centred hero with a soft radial spotlight, badge and dual call-to-action.

Split Hero

Split Hero

Original

A two-column hero pairing a headline and CTAs with a product mock and social proof.

Gradient Spotlight Hero

Gradient Spotlight Hero

Original

A minimal centred hero with a soft gradient-mesh backdrop, announcement pill and dual call-to-action buttons.

App Preview Hero

App Preview Hero

Original

A centred hero that pairs headline copy with a realistic product dashboard mock built entirely from markup, complete with browser chrome and a floating notification card.

Waitlist Capture Hero

Waitlist Capture Hero

Original

A dark, focused hero with an inline email capture form and avatar social proof, ready for pre-launch waitlists.

Image Backdrop Hero

Image Backdrop Hero

Original

A cinematic full-bleed hero with a layered image-style backdrop, overlaid left-aligned copy and a trusted-by logos strip.

Gradient Mesh Hero with Staggered Text Reveal

Gradient Mesh Hero with Staggered Text Reveal

Original

A full-screen hero pairing a drifting animated gradient-mesh backdrop with a word-by-word staggered blur-in headline and a logo marquee under the fold.

Typewriter Rotating Words Hero

Typewriter Rotating Words Hero

Original

A hero whose headline cycles through rotating words with a live typewriter caret over an animated grid backdrop, degrading to a gentle word swap when reduced motion is on.

Floating UI Cards Hero with Mouse Parallax

Floating UI Cards Hero with Mouse Parallax

Original

A hero with glassy floating UI cards that drift continuously and shift on mouse-parallax depth around a staggered centre headline.

Aurora Hero with Shimmer CTA and Logo Marquee

Aurora Hero with Shimmer CTA and Logo Marquee

Original

A dark hero with animated aurora light beams, a shimmering sweep across the primary CTA, and dual-direction logo marquees beneath the fold.

Gradient Conversion Hero

Gradient Conversion Hero

MIT

Centered, full-screen marketing hero with a colour-accent headline word, supporting copy, and dual primary/secondary CTA buttons. Dark-mode ready and dependency-free.

Split Showcase Hero

Split Showcase Hero

MIT

Two-column hero on a dark background with a headline, paragraph, and two buttons beside a gradient media panel. Responsive column stack on mobile.