Web InnoventixFreeCode

Icons Stat

Original · free

icon stat grid

byWeb InnoventixReact + Tailwind
statxiconsstats
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/statx-icons.json
statx-icons.tsx
"use client";

import { useEffect, useRef, useState } from "react";
import type { ComponentType, KeyboardEvent, SVGProps } from "react";
import { motion, useInView, useReducedMotion } from "motion/react";

/* ------------------------------------------------------------------ */
/*  Inline SVG icons (no icon libraries)                               */
/* ------------------------------------------------------------------ */

function IconBase(props: SVGProps<SVGSVGElement>) {
  return (
    <svg
      width={24}
      height={24}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={1.75}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
      {...props}
    />
  );
}

function IconUsers(props: SVGProps<SVGSVGElement>) {
  return (
    <IconBase {...props}>
      <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
      <circle cx="9" cy="7" r="4" />
      <path d="M23 21v-2a4 4 0 0 0-3-3.87" />
      <path d="M16 3.13a4 4 0 0 1 0 7.75" />
    </IconBase>
  );
}

function IconActivity(props: SVGProps<SVGSVGElement>) {
  return (
    <IconBase {...props}>
      <polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />
    </IconBase>
  );
}

function IconRocket(props: SVGProps<SVGSVGElement>) {
  return (
    <IconBase {...props}>
      <path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09Z" />
      <path d="M12 15l-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2Z" />
      <path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" />
      <path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" />
    </IconBase>
  );
}

function IconBolt(props: SVGProps<SVGSVGElement>) {
  return (
    <IconBase {...props}>
      <polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />
    </IconBase>
  );
}

function IconShield(props: SVGProps<SVGSVGElement>) {
  return (
    <IconBase {...props}>
      <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10Z" />
      <path d="m9 12 2 2 4-4" />
    </IconBase>
  );
}

function IconChart(props: SVGProps<SVGSVGElement>) {
  return (
    <IconBase {...props}>
      <line x1="6" y1="20" x2="6" y2="14" />
      <line x1="12" y1="20" x2="12" y2="4" />
      <line x1="18" y1="20" x2="18" y2="10" />
    </IconBase>
  );
}

function IconArrowUp(props: SVGProps<SVGSVGElement>) {
  return (
    <IconBase {...props}>
      <path d="M12 19V5" />
      <path d="m5 12 7-7 7 7" />
    </IconBase>
  );
}

function IconArrowDown(props: SVGProps<SVGSVGElement>) {
  return (
    <IconBase {...props}>
      <path d="M12 5v14" />
      <path d="m19 12-7 7-7-7" />
    </IconBase>
  );
}

/* ------------------------------------------------------------------ */
/*  Data model                                                         */
/* ------------------------------------------------------------------ */

const PERIODS = [
  { id: "7d", label: "last 7 days" },
  { id: "30d", label: "last 30 days" },
  { id: "90d", label: "last 90 days" },
] as const;

type PeriodId = (typeof PERIODS)[number]["id"];

type Format = "int" | "compact" | "ms" | "percent" | "currency";

type Accent = {
  chip: string;
  icon: string;
  bar: string;
  ring: string;
};

type PeriodData = {
  value: number;
  delta: string;
  dir: "up" | "down";
  good: boolean;
};

type Metric = {
  id: string;
  label: string;
  hint: string;
  format: Format;
  decimals: number;
  icon: ComponentType<SVGProps<SVGSVGElement>>;
  accent: Accent;
  series: number[];
  data: Record<PeriodId, PeriodData>;
};

const ACCENTS: Record<string, Accent> = {
  indigo: {
    chip: "bg-indigo-50 dark:bg-indigo-500/10",
    icon: "text-indigo-600 dark:text-indigo-300",
    bar: "bg-indigo-500/70 dark:bg-indigo-400/70",
    ring: "hover:border-indigo-300 dark:hover:border-indigo-500/40",
  },
  violet: {
    chip: "bg-violet-50 dark:bg-violet-500/10",
    icon: "text-violet-600 dark:text-violet-300",
    bar: "bg-violet-500/70 dark:bg-violet-400/70",
    ring: "hover:border-violet-300 dark:hover:border-violet-500/40",
  },
  sky: {
    chip: "bg-sky-50 dark:bg-sky-500/10",
    icon: "text-sky-600 dark:text-sky-300",
    bar: "bg-sky-500/70 dark:bg-sky-400/70",
    ring: "hover:border-sky-300 dark:hover:border-sky-500/40",
  },
  amber: {
    chip: "bg-amber-50 dark:bg-amber-500/10",
    icon: "text-amber-600 dark:text-amber-300",
    bar: "bg-amber-500/70 dark:bg-amber-400/70",
    ring: "hover:border-amber-300 dark:hover:border-amber-500/40",
  },
  emerald: {
    chip: "bg-emerald-50 dark:bg-emerald-500/10",
    icon: "text-emerald-600 dark:text-emerald-300",
    bar: "bg-emerald-500/70 dark:bg-emerald-400/70",
    ring: "hover:border-emerald-300 dark:hover:border-emerald-500/40",
  },
  rose: {
    chip: "bg-rose-50 dark:bg-rose-500/10",
    icon: "text-rose-600 dark:text-rose-300",
    bar: "bg-rose-500/70 dark:bg-rose-400/70",
    ring: "hover:border-rose-300 dark:hover:border-rose-500/40",
  },
};

const METRICS: Metric[] = [
  {
    id: "developers",
    label: "Active developers",
    hint: "Weekly active accounts across every region",
    format: "int",
    decimals: 0,
    icon: IconUsers,
    accent: ACCENTS.indigo,
    series: [0.4, 0.5, 0.45, 0.6, 0.55, 0.7, 0.82, 0.94],
    data: {
      "7d": { value: 12480, delta: "+8.2%", dir: "up", good: true },
      "30d": { value: 41900, delta: "+11.4%", dir: "up", good: true },
      "90d": { value: 118300, delta: "+18.9%", dir: "up", good: true },
    },
  },
  {
    id: "requests",
    label: "API requests",
    hint: "Successful calls to the REST and GraphQL edge",
    format: "compact",
    decimals: 0,
    icon: IconActivity,
    accent: ACCENTS.violet,
    series: [0.5, 0.55, 0.6, 0.5, 0.66, 0.72, 0.78, 0.88],
    data: {
      "7d": { value: 4230000, delta: "+6.1%", dir: "up", good: true },
      "30d": { value: 18600000, delta: "+9.7%", dir: "up", good: true },
      "90d": { value: 52400000, delta: "+14.2%", dir: "up", good: true },
    },
  },
  {
    id: "deploys",
    label: "Deploys shipped",
    hint: "Production releases that passed CI and rolled out",
    format: "int",
    decimals: 0,
    icon: IconRocket,
    accent: ACCENTS.sky,
    series: [0.3, 0.5, 0.4, 0.6, 0.72, 0.6, 0.82, 0.9],
    data: {
      "7d": { value: 1204, delta: "+12.3%", dir: "up", good: true },
      "30d": { value: 5380, delta: "+8.9%", dir: "up", good: true },
      "90d": { value: 16240, delta: "+15.6%", dir: "up", good: true },
    },
  },
  {
    id: "latency",
    label: "p95 latency",
    hint: "95th-percentile response time at the API gateway",
    format: "ms",
    decimals: 0,
    icon: IconBolt,
    accent: ACCENTS.amber,
    series: [0.62, 0.5, 0.56, 0.44, 0.5, 0.4, 0.42, 0.38],
    data: {
      "7d": { value: 128, delta: "-6.4%", dir: "down", good: true },
      "30d": { value: 134, delta: "-3.1%", dir: "down", good: true },
      "90d": { value: 141, delta: "+2.2%", dir: "up", good: false },
    },
  },
  {
    id: "uptime",
    label: "Service uptime",
    hint: "Availability across all production services",
    format: "percent",
    decimals: 2,
    icon: IconShield,
    accent: ACCENTS.emerald,
    series: [0.85, 0.9, 0.88, 0.92, 0.95, 0.93, 0.97, 0.99],
    data: {
      "7d": { value: 99.98, delta: "+0.01 pts", dir: "up", good: true },
      "30d": { value: 99.96, delta: "+0.02 pts", dir: "up", good: true },
      "90d": { value: 99.95, delta: "-0.01 pts", dir: "down", good: false },
    },
  },
  {
    id: "mrr",
    label: "Recurring revenue",
    hint: "Net new MRR booked inside this window",
    format: "currency",
    decimals: 0,
    icon: IconChart,
    accent: ACCENTS.rose,
    series: [0.4, 0.45, 0.5, 0.55, 0.62, 0.7, 0.8, 0.92],
    data: {
      "7d": { value: 48200, delta: "+4.1%", dir: "up", good: true },
      "30d": { value: 186400, delta: "+7.3%", dir: "up", good: true },
      "90d": { value: 540900, delta: "+12.8%", dir: "up", good: true },
    },
  },
];

/* ------------------------------------------------------------------ */
/*  Formatting helpers                                                 */
/* ------------------------------------------------------------------ */

function formatCompact(n: number): string {
  if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
  if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
  return Math.round(n).toString();
}

function formatValue(n: number, format: Format, decimals: number): string {
  switch (format) {
    case "int":
      return Math.round(n).toLocaleString("en-US");
    case "compact":
      return formatCompact(n);
    case "ms":
      return `${Math.round(n)} ms`;
    case "percent":
      return `${n.toFixed(decimals)}%`;
    case "currency":
      return `$${Math.round(n).toLocaleString("en-US")}`;
    default:
      return String(n);
  }
}

/* ------------------------------------------------------------------ */
/*  Count-up hook (respects reduced motion, retargets on change)       */
/* ------------------------------------------------------------------ */

function useCountUp(
  target: number,
  active: boolean,
  reduced: boolean,
): number {
  const [display, setDisplay] = useState(0);
  const fromRef = useRef(0);
  const rafRef = useRef<number | null>(null);

  useEffect(() => {
    if (!active) return;

    if (reduced) {
      fromRef.current = target;
      setDisplay(target);
      return;
    }

    const from = fromRef.current;
    const duration = 950;
    const startTime = performance.now();

    const tick = (now: number) => {
      const t = Math.min(1, (now - startTime) / duration);
      const eased = 1 - Math.pow(1 - t, 3);
      const current = from + (target - from) * eased;
      fromRef.current = current;
      setDisplay(current);
      if (t < 1) {
        rafRef.current = requestAnimationFrame(tick);
      }
    };

    rafRef.current = requestAnimationFrame(tick);
    return () => {
      if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
    };
  }, [target, active, reduced]);

  return display;
}

/* ------------------------------------------------------------------ */
/*  Delta pill                                                         */
/* ------------------------------------------------------------------ */

function Delta({ data }: { data: PeriodData }) {
  const tone = data.good
    ? "bg-emerald-50 text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-300"
    : "bg-rose-50 text-rose-700 dark:bg-rose-500/10 dark:text-rose-300";
  return (
    <span
      className={`inline-flex items-center gap-1 rounded-full px-2 py-1 text-xs font-semibold tabular-nums ${tone}`}
    >
      {data.dir === "up" ? (
        <IconArrowUp className="h-3 w-3" strokeWidth={2.25} />
      ) : (
        <IconArrowDown className="h-3 w-3" strokeWidth={2.25} />
      )}
      <span className="sr-only">
        {data.good ? "improved, " : "declined, "}
        {data.dir === "up" ? "up " : "down "}
      </span>
      {data.delta}
    </span>
  );
}

/* ------------------------------------------------------------------ */
/*  Stat card                                                          */
/* ------------------------------------------------------------------ */

function StatCard({
  metric,
  period,
  periodLabel,
  inView,
  reduced,
  index,
}: {
  metric: Metric;
  period: PeriodId;
  periodLabel: string;
  inView: boolean;
  reduced: boolean;
  index: number;
}) {
  const d = metric.data[period];
  const display = useCountUp(d.value, inView, reduced);
  const Icon = metric.icon;

  return (
    <motion.div
      className={`group relative flex flex-col gap-5 rounded-2xl border border-slate-200 bg-white p-6 transition-colors duration-300 dark:border-slate-800 dark:bg-slate-900/50 ${metric.accent.ring} hover:shadow-lg hover:shadow-slate-900/5 dark:hover:shadow-black/30`}
      initial={reduced ? false : { opacity: 0, y: 16 }}
      animate={
        reduced
          ? { opacity: 1, y: 0 }
          : inView
            ? { opacity: 1, y: 0 }
            : { opacity: 0, y: 16 }
      }
      transition={{ duration: 0.5, delay: index * 0.06, ease: "easeOut" }}
    >
      <div className="flex items-start justify-between">
        <span
          className={`flex h-11 w-11 items-center justify-center rounded-xl ${metric.accent.chip}`}
        >
          <Icon className={`h-5 w-5 ${metric.accent.icon}`} />
        </span>
        <Delta data={d} />
      </div>

      <div>
        <div className="text-3xl font-semibold tracking-tight tabular-nums text-slate-900 dark:text-white sm:text-4xl">
          {formatValue(display, metric.format, metric.decimals)}
        </div>
        <div className="mt-1 text-sm font-medium text-slate-500 dark:text-slate-400">
          {metric.label}
        </div>
      </div>

      <div
        className="flex h-12 items-end gap-1"
        aria-hidden="true"
      >
        {metric.series.map((v, i) => (
          <motion.span
            key={i}
            className={`flex-1 rounded-sm ${metric.accent.bar}`}
            style={{ height: `${Math.max(10, v * 100)}%`, transformOrigin: "bottom" }}
            initial={reduced ? false : { scaleY: 0, opacity: 0 }}
            animate={
              reduced
                ? { scaleY: 1, opacity: 1 }
                : inView
                  ? { scaleY: 1, opacity: 1 }
                  : { scaleY: 0, opacity: 0 }
            }
            transition={{
              duration: 0.5,
              delay: 0.15 + index * 0.06 + i * 0.04,
              ease: "easeOut",
            }}
          />
        ))}
      </div>

      <div className="border-t border-slate-100 pt-4 dark:border-slate-800/80">
        <p className="text-xs leading-relaxed text-slate-500 dark:text-slate-400">
          {metric.hint}
        </p>
        <p className="mt-1 text-[11px] font-medium uppercase tracking-wide text-slate-400 dark:text-slate-500">
          vs previous {periodLabel.replace("last ", "")}
        </p>
      </div>
    </motion.div>
  );
}

/* ------------------------------------------------------------------ */
/*  Main component                                                     */
/* ------------------------------------------------------------------ */

export default function StatxIcons() {
  const reduced = useReducedMotion() ?? false;
  const [period, setPeriod] = useState<PeriodId>("30d");
  const gridRef = useRef<HTMLDivElement>(null);
  const inView = useInView(gridRef, { once: true, margin: "-80px" });
  const tabRefs = useRef<Array<HTMLButtonElement | null>>([]);

  const activeLabel =
    PERIODS.find((p) => p.id === period)?.label ?? PERIODS[0].label;

  const onTabKeyDown = (
    event: KeyboardEvent<HTMLButtonElement>,
    index: number,
  ) => {
    let next = index;
    if (event.key === "ArrowRight" || event.key === "ArrowDown") {
      next = (index + 1) % PERIODS.length;
    } else if (event.key === "ArrowLeft" || event.key === "ArrowUp") {
      next = (index - 1 + PERIODS.length) % PERIODS.length;
    } else if (event.key === "Home") {
      next = 0;
    } else if (event.key === "End") {
      next = PERIODS.length - 1;
    } else {
      return;
    }
    event.preventDefault();
    setPeriod(PERIODS[next].id);
    tabRefs.current[next]?.focus();
  };

  return (
    <section
      aria-labelledby="statx-heading"
      className="relative w-full overflow-hidden bg-white px-6 py-20 text-slate-900 dark:bg-slate-950 dark:text-white sm:py-24 lg:px-8"
    >
      <style>{`
        @keyframes statx-pulse {
          0%, 100% { opacity: 1; transform: scale(1); }
          50% { opacity: 0.3; transform: scale(1.9); }
        }
        .statx-live-dot { animation: statx-pulse 2.4s ease-in-out infinite; }
        @media (prefers-reduced-motion: reduce) {
          .statx-live-dot { animation: none; }
        }
      `}</style>

      {/* atmospheric accent */}
      <div
        aria-hidden="true"
        className="pointer-events-none absolute -top-24 left-1/2 h-72 w-[42rem] -translate-x-1/2 rounded-full bg-gradient-to-r from-indigo-200/40 via-violet-200/30 to-sky-200/40 blur-3xl dark:from-indigo-600/10 dark:via-violet-600/10 dark:to-sky-600/10"
      />

      <div className="relative mx-auto max-w-6xl">
        {/* header */}
        <div className="flex flex-col gap-8 lg:flex-row lg:items-end lg:justify-between">
          <div className="max-w-xl">
            <div className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-slate-50 px-3 py-1 text-xs font-medium text-slate-600 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-300">
              <span className="relative flex h-2 w-2">
                <span className="statx-live-dot absolute inline-flex h-full w-full rounded-full bg-emerald-400" />
                <span className="relative inline-flex h-2 w-2 rounded-full bg-emerald-500" />
              </span>
              Live · updated just now
            </div>

            <h2
              id="statx-heading"
              className="mt-4 text-3xl font-semibold tracking-tight text-slate-900 dark:text-white sm:text-4xl"
            >
              Everything moving through your stack, at a glance
            </h2>
            <p className="mt-3 text-base leading-relaxed text-slate-600 dark:text-slate-400">
              Six core signals, refreshed every minute. Switch the window to see
              how growth, reliability, and revenue trend together.
            </p>
          </div>

          {/* segmented control (radiogroup) */}
          <div className="shrink-0">
            <div
              id="statx-window-label"
              className="mb-2 text-xs font-medium uppercase tracking-wide text-slate-400 dark:text-slate-500"
            >
              Time window
            </div>
            <div
              role="radiogroup"
              aria-labelledby="statx-window-label"
              className="inline-flex rounded-full border border-slate-200 bg-slate-100 p-1 dark:border-slate-800 dark:bg-slate-900"
            >
              {PERIODS.map((p, i) => {
                const selected = p.id === period;
                return (
                  <button
                    key={p.id}
                    ref={(el) => {
                      tabRefs.current[i] = el;
                    }}
                    type="button"
                    role="radio"
                    aria-checked={selected}
                    tabIndex={selected ? 0 : -1}
                    onClick={() => setPeriod(p.id)}
                    onKeyDown={(e) => onTabKeyDown(e, i)}
                    className={`rounded-full px-4 py-1.5 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-100 dark:focus-visible:ring-offset-slate-900 ${
                      selected
                        ? "bg-white text-slate-900 shadow-sm dark:bg-slate-950 dark:text-white"
                        : "text-slate-500 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white"
                    }`}
                  >
                    {p.label.replace("last ", "")}
                  </button>
                );
              })}
            </div>
          </div>
        </div>

        <p className="sr-only" role="status" aria-live="polite">
          Showing platform metrics for the {activeLabel}.
        </p>

        {/* grid */}
        <div
          ref={gridRef}
          className="mt-12 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3"
        >
          {METRICS.map((metric, index) => (
            <StatCard
              key={metric.id}
              metric={metric}
              period={period}
              periodLabel={activeLabel}
              inView={inView}
              reduced={reduced}
              index={index}
            />
          ))}
        </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 →
Four Stat Metric Band

Four Stat Metric Band

Original

A four-column metric band with gradient figures inside a hairline-divided card, ideal for headline numbers like uptime, latency and growth.

Labelled Stats With Dividers

Labelled Stats With Dividers

Original

Three centred stats separated by responsive dividers, each pairing a headline figure with an uppercase label and supporting detail.

Trusted By Logo Cloud

Trusted By Logo Cloud

Original

A responsive six-up logo strip using simple inline-SVG marks and wordmarks with a grayscale-to-colour hover, for a trusted-by section.

Hero Stat With Supporting Copy

Hero Stat With Supporting Copy

Original

A two-column layout pairing one oversized hero statistic with a supporting paragraph and a checklist card explaining the number.

Count-up stats reveal

Count-up stats reveal

Original

A four-card stat band whose gradient numbers count up from zero as the section scrolls into view, with staggered blur-in cards, a pulsing eyebrow, floating aurora blobs and a shimmering underline sweep.

Animated SVG progress rings

Animated SVG progress rings

Original

Four circular SVG progress rings that draw their gradient arcs and count up to their target percentage the moment they enter view, over a slowly rotating conic halo with a soft glow pulse behind each ring.

Staggered stat band with marquee ticker

Staggered stat band with marquee ticker

Original

A dark stat band where 3D-tilt cards stagger into view with count-up figures over an animated gradient mesh, finished by an infinite marquee ticker of highlight chips that pauses on hover.

Counter Row Stat

Counter Row Stat

Original

animated counter stats row

Cards Stat

Cards Stat

Original

stat cards with icons

Gradient Stat

Gradient Stat

Original

gradient stats band

Split Stat

Split Stat

Original

stats beside copy

Progress Stat

Progress Stat

Original

stats with progress rings