Web InnoventixFreeCode

Donut Stat

Original · free

stats with donut charts

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

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

/* ---------------------------------- types --------------------------------- */

type RangeId = "7d" | "30d" | "90d";

interface Palette {
  stroke: string;
  bg: string;
  text: string;
}

interface Source {
  id: string;
  label: string;
  value: number;
  color: Palette;
}

interface Kpi {
  id: string;
  label: string;
  value: number;
  unit: string;
  decimals: number;
  ring: number; // 0..100 fill of the donut
  delta: number;
  goodWhenDown: boolean;
  caption: string;
  color: Palette;
}

interface RangeData {
  sources: Source[];
  kpis: Kpi[];
}

/* -------------------------------- palettes -------------------------------- */

const INDIGO: Palette = { stroke: "stroke-indigo-500", bg: "bg-indigo-500", text: "text-indigo-500" };
const VIOLET: Palette = { stroke: "stroke-violet-500", bg: "bg-violet-500", text: "text-violet-500" };
const SKY: Palette = { stroke: "stroke-sky-500", bg: "bg-sky-500", text: "text-sky-500" };
const EMERALD: Palette = { stroke: "stroke-emerald-500", bg: "bg-emerald-500", text: "text-emerald-500" };
const AMBER: Palette = { stroke: "stroke-amber-500", bg: "bg-amber-500", text: "text-amber-500" };

/* ---------------------------------- data ---------------------------------- */

const RANGES: { id: RangeId; label: string; full: string }[] = [
  { id: "7d", label: "7D", full: "Last 7 days" },
  { id: "30d", label: "30D", full: "Last 30 days" },
  { id: "90d", label: "90D", full: "Last 90 days" },
];

function makeSources(v: [number, number, number, number, number]): Source[] {
  return [
    { id: "organic", label: "Organic search", value: v[0], color: INDIGO },
    { id: "direct", label: "Direct", value: v[1], color: VIOLET },
    { id: "referral", label: "Referral", value: v[2], color: SKY },
    { id: "social", label: "Social", value: v[3], color: EMERALD },
    { id: "email", label: "Email", value: v[4], color: AMBER },
  ];
}

const DATA: Record<RangeId, RangeData> = {
  "7d": {
    sources: makeSources([5820, 3110, 1840, 1260, 640]),
    kpis: [
      { id: "goal", label: "Goal completion", value: 68, unit: "%", decimals: 0, ring: 68, delta: 5.2, goodWhenDown: false, caption: "of monthly target", color: INDIGO },
      { id: "conv", label: "Conversion rate", value: 4.8, unit: "%", decimals: 1, ring: 68.6, delta: 0.6, goodWhenDown: false, caption: "against a 7% goal", color: VIOLET },
      { id: "bounce", label: "Bounce rate", value: 41.2, unit: "%", decimals: 1, ring: 41.2, delta: -1.4, goodWhenDown: true, caption: "sessions with no action", color: AMBER },
      { id: "return", label: "Returning visitors", value: 54, unit: "%", decimals: 0, ring: 54, delta: 2.0, goodWhenDown: false, caption: "of all visitors", color: EMERALD },
    ],
  },
  "30d": {
    sources: makeSources([24100, 12760, 7420, 5180, 2610]),
    kpis: [
      { id: "goal", label: "Goal completion", value: 72, unit: "%", decimals: 0, ring: 72, delta: 8.1, goodWhenDown: false, caption: "of monthly target", color: INDIGO },
      { id: "conv", label: "Conversion rate", value: 5.1, unit: "%", decimals: 1, ring: 72.9, delta: 0.9, goodWhenDown: false, caption: "against a 7% goal", color: VIOLET },
      { id: "bounce", label: "Bounce rate", value: 38.4, unit: "%", decimals: 1, ring: 38.4, delta: -2.6, goodWhenDown: true, caption: "sessions with no action", color: AMBER },
      { id: "return", label: "Returning visitors", value: 57, unit: "%", decimals: 0, ring: 57, delta: 3.4, goodWhenDown: false, caption: "of all visitors", color: EMERALD },
    ],
  },
  "90d": {
    sources: makeSources([71300, 38200, 21100, 15400, 7900]),
    kpis: [
      { id: "goal", label: "Goal completion", value: 79, unit: "%", decimals: 0, ring: 79, delta: 12.4, goodWhenDown: false, caption: "of monthly target", color: INDIGO },
      { id: "conv", label: "Conversion rate", value: 5.6, unit: "%", decimals: 1, ring: 80, delta: 1.4, goodWhenDown: false, caption: "against a 7% goal", color: VIOLET },
      { id: "bounce", label: "Bounce rate", value: 36.1, unit: "%", decimals: 1, ring: 36.1, delta: -3.8, goodWhenDown: true, caption: "sessions with no action", color: AMBER },
      { id: "return", label: "Returning visitors", value: 61, unit: "%", decimals: 0, ring: 61, delta: 4.9, goodWhenDown: false, caption: "of all visitors", color: EMERALD },
    ],
  },
};

const ALL_IDS = ["organic", "direct", "referral", "social", "email"];

const EASE: [number, number, number, number] = [0.22, 1, 0.36, 1];

/* ------------------------------ number helper ----------------------------- */

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

function AnimatedNumber({
  value,
  decimals = 0,
  reduced,
}: {
  value: number;
  decimals?: number;
  reduced: boolean;
}): ReactNode {
  const [display, setDisplay] = useState<number>(reduced ? value : 0);
  const from = useRef<number>(reduced ? value : 0);

  useEffect(() => {
    if (reduced) {
      from.current = value;
      setDisplay(value);
      return;
    }
    const start = from.current;
    const delta = value - start;
    const duration = 850;
    let raf = 0;
    let t0 = 0;
    const ease = (x: number): number => 1 - Math.pow(1 - x, 3);
    const step = (ts: number): void => {
      if (t0 === 0) t0 = ts;
      const p = Math.min((ts - t0) / duration, 1);
      const cur = start + delta * ease(p);
      from.current = cur;
      setDisplay(cur);
      if (p < 1) raf = requestAnimationFrame(step);
    };
    raf = requestAnimationFrame(step);
    return () => cancelAnimationFrame(raf);
  }, [value, reduced]);

  return <>{fmt(display, decimals)}</>;
}

/* --------------------------------- arrows --------------------------------- */

function DeltaIcon({ up }: { up: boolean }): ReactNode {
  return (
    <svg viewBox="0 0 12 12" width="12" height="12" aria-hidden="true" className="shrink-0">
      <path
        d={up ? "M6 2.5 10 8H2z" : "M6 9.5 2 4h8z"}
        fill="currentColor"
      />
    </svg>
  );
}

/* -------------------------------- KPI card -------------------------------- */

function KpiCard({
  kpi,
  index,
  reduced,
}: {
  kpi: Kpi;
  index: number;
  reduced: boolean;
}): ReactNode {
  const finalDash = `${kpi.ring} ${100 - kpi.ring}`;
  const good = kpi.goodWhenDown ? kpi.delta < 0 : kpi.delta > 0;
  const deltaUp = kpi.delta > 0;

  return (
    <div className="group relative flex flex-col rounded-2xl border border-slate-200 bg-white/80 p-5 backdrop-blur transition-shadow hover:shadow-lg hover:shadow-slate-900/5 dark:border-slate-800 dark:bg-slate-900/70 dark:hover:shadow-black/40">
      <div className="flex items-start justify-between gap-3">
        <span className="text-[0.8rem] font-medium leading-tight text-slate-500 dark:text-slate-400">
          {kpi.label}
        </span>
        <span
          className={[
            "inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[0.7rem] font-semibold tabular-nums",
            good
              ? "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400"
              : "bg-rose-500/10 text-rose-600 dark:text-rose-400",
          ].join(" ")}
        >
          <DeltaIcon up={deltaUp} />
          {deltaUp ? "+" : ""}
          {fmt(kpi.delta, 1)}
        </span>
      </div>

      <div className="mt-3 flex items-center gap-4">
        <div className="relative h-[76px] w-[76px] shrink-0">
          <svg viewBox="0 0 100 100" className="h-full w-full -rotate-90">
            <circle
              cx="50"
              cy="50"
              r="40"
              fill="none"
              strokeWidth="9"
              className="stroke-slate-200/80 dark:stroke-slate-700/70"
            />
            <motion.circle
              cx="50"
              cy="50"
              r="40"
              fill="none"
              strokeWidth="9"
              strokeLinecap="round"
              pathLength={100}
              className={kpi.color.stroke}
              initial={{ strokeDasharray: reduced ? finalDash : "0 100" }}
              animate={{ strokeDasharray: finalDash }}
              transition={{
                duration: reduced ? 0 : 0.9,
                ease: EASE,
                delay: reduced ? 0 : 0.15 + index * 0.08,
              }}
            />
          </svg>
          <div className="absolute inset-0 flex flex-col items-center justify-center">
            <span className="text-lg font-semibold tabular-nums text-slate-900 dark:text-slate-50">
              <AnimatedNumber value={kpi.value} decimals={kpi.decimals} reduced={reduced} />
              <span className="text-[0.7em] text-slate-400 dark:text-slate-500">{kpi.unit}</span>
            </span>
          </div>
        </div>

        <p className="text-[0.78rem] leading-snug text-slate-500 dark:text-slate-400">
          {kpi.caption}
        </p>
      </div>
    </div>
  );
}

/* ------------------------------ main component ----------------------------- */

export default function StatxDonut(): ReactNode {
  const prefersReduced = useReducedMotion();
  const reduced = prefersReduced === true;
  const uid = useId();

  const [range, setRange] = useState<RangeId>("30d");
  const [activeIds, setActiveIds] = useState<string[]>(ALL_IDS);
  const [hovered, setHovered] = useState<string | null>(null);

  const data = DATA[range];
  const sources = data.sources;

  const active = sources.filter((s) => activeIds.includes(s.id));
  const total = active.reduce((sum, s) => sum + s.value, 0);

  function toggle(id: string): void {
    setActiveIds((prev) => {
      const on = prev.includes(id);
      if (on) {
        if (prev.length === 1) return prev; // keep at least one segment
        return prev.filter((x) => x !== id);
      }
      return ALL_IDS.filter((x) => x === id || prev.includes(x));
    });
  }

  // Build arc geometry (pathLength normalised to 100).
  let cum = 0;
  const gap = active.length > 1 ? 1.6 : 0;
  const arcs = sources.map((s) => {
    const isActive = activeIds.includes(s.id);
    const pct = isActive && total > 0 ? (s.value / total) * 100 : 0;
    const visible = Math.max(pct - gap, 0);
    const offset = -cum;
    if (isActive) cum += pct;
    return { source: s, isActive, pct, visible, offset };
  });

  const hoveredSource = hovered ? sources.find((s) => s.id === hovered) ?? null : null;
  const hoveredActive = hoveredSource ? activeIds.includes(hoveredSource.id) : false;
  const hoveredPct =
    hoveredSource && hoveredActive && total > 0 ? (hoveredSource.value / total) * 100 : 0;

  const arcTransition = {
    strokeDasharray: { duration: reduced ? 0 : 0.7, ease: EASE },
    strokeDashoffset: { duration: reduced ? 0 : 0.7, ease: EASE },
    opacity: { duration: reduced ? 0 : 0.25 },
    strokeWidth: { duration: reduced ? 0 : 0.25 },
  };

  return (
    <section className="relative w-full overflow-hidden bg-gradient-to-b from-slate-50 to-white px-4 py-16 text-slate-900 sm:px-6 md:py-24 dark:from-slate-950 dark:to-slate-900 dark:text-slate-50">
      <style>{`
        @keyframes statxdonut-orbit {
          to { transform: rotate(360deg); }
        }
        @keyframes statxdonut-pulse {
          0%, 100% { opacity: 0.55; transform: scale(1); }
          50% { opacity: 1; transform: scale(1.35); }
        }
        .statxdonut-orbit {
          transform-origin: 50% 50%;
          animation: statxdonut-orbit 26s linear infinite;
        }
        .statxdonut-dot {
          animation: statxdonut-pulse 2.4s ease-in-out infinite;
        }
        @media (prefers-reduced-motion: reduce) {
          .statxdonut-orbit, .statxdonut-dot { animation: none !important; }
        }
      `}</style>

      {/* atmospheric blobs */}
      <div
        aria-hidden="true"
        className="pointer-events-none absolute -left-24 top-10 h-72 w-72 rounded-full bg-indigo-400/20 blur-3xl dark:bg-indigo-600/15"
      />
      <div
        aria-hidden="true"
        className="pointer-events-none absolute -right-16 bottom-0 h-72 w-72 rounded-full bg-violet-400/15 blur-3xl dark:bg-violet-700/15"
      />

      <div className="relative mx-auto max-w-6xl">
        {/* ------------------------------- header ------------------------------ */}
        <header className="flex flex-col gap-6 sm:flex-row sm:items-end sm:justify-between">
          <div>
            <span className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white/70 px-3 py-1 text-[0.72rem] font-semibold uppercase tracking-[0.14em] text-slate-500 dark:border-slate-700 dark:bg-slate-800/60 dark:text-slate-400">
              <span className="relative flex h-1.5 w-1.5">
                <span className="statxdonut-dot absolute inline-flex h-full w-full rounded-full bg-emerald-500" />
                <span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-emerald-500" />
              </span>
              Acquisition report
            </span>
            <h2 className="mt-3 text-2xl font-semibold tracking-tight sm:text-3xl">
              Audience overview
            </h2>
            <p className="mt-1.5 max-w-md text-sm text-slate-500 dark:text-slate-400">
              Sessions by channel, with goal and engagement signals. Toggle a channel to
              recompute the mix.
            </p>
          </div>

          {/* time-range radiogroup */}
          <div
            role="radiogroup"
            aria-label="Select reporting window"
            className="inline-flex shrink-0 rounded-full border border-slate-200 bg-white/70 p-1 shadow-sm dark:border-slate-700 dark:bg-slate-800/60"
          >
            {RANGES.map((r) => {
              const on = range === r.id;
              return (
                <label
                  key={r.id}
                  className={[
                    "relative cursor-pointer select-none rounded-full px-4 py-1.5 text-sm font-semibold transition-colors",
                    "peer-focus-visible:outline-none",
                    on
                      ? "text-white"
                      : "text-slate-500 hover:text-slate-800 dark:text-slate-400 dark:hover:text-slate-100",
                  ].join(" ")}
                >
                  <input
                    type="radio"
                    name={`${uid}-range`}
                    value={r.id}
                    checked={on}
                    onChange={() => setRange(r.id)}
                    className="peer sr-only"
                    aria-label={r.full}
                  />
                  {on && (
                    <motion.span
                      layoutId={`${uid}-pill`}
                      className="absolute inset-0 -z-10 rounded-full bg-indigo-600 dark:bg-indigo-500"
                      transition={{ duration: reduced ? 0 : 0.3, ease: EASE }}
                    />
                  )}
                  <span className="pointer-events-none absolute inset-0 rounded-full ring-indigo-500 peer-focus-visible:ring-2 peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-white dark:peer-focus-visible:ring-offset-slate-900" />
                  {r.label}
                </label>
              );
            })}
          </div>
        </header>

        {/* --------------------------- main donut card -------------------------- */}
        <div className="mt-8 grid gap-6 rounded-3xl border border-slate-200 bg-white/70 p-6 shadow-sm backdrop-blur md:grid-cols-2 md:p-8 dark:border-slate-800 dark:bg-slate-900/60">
          {/* donut */}
          <div className="flex items-center justify-center">
            <div className="relative aspect-square w-full max-w-[280px]">
              <svg
                viewBox="0 0 100 100"
                className="h-full w-full"
                role="img"
                aria-label={`Donut chart of traffic sources. ${fmt(total, 0)} total sessions across ${active.length} active ${active.length === 1 ? "channel" : "channels"}.`}
              >
                {/* decorative orbiting ticks */}
                <g className="statxdonut-orbit" aria-hidden="true">
                  <circle
                    cx="50"
                    cy="50"
                    r="46"
                    fill="none"
                    strokeWidth="0.6"
                    strokeDasharray="0.4 3.2"
                    strokeLinecap="round"
                    className="stroke-slate-300 dark:stroke-slate-700"
                  />
                </g>

                <g transform="rotate(-90 50 50)">
                  {/* track */}
                  <circle
                    cx="50"
                    cy="50"
                    r="38"
                    fill="none"
                    strokeWidth="11"
                    className="stroke-slate-200/70 dark:stroke-slate-800"
                  />
                  {arcs.map((a) => {
                    const dimmed = hovered !== null && hovered !== a.source.id;
                    return (
                      <motion.circle
                        key={a.source.id}
                        cx="50"
                        cy="50"
                        r="38"
                        fill="none"
                        strokeLinecap="butt"
                        pathLength={100}
                        className={a.source.color.stroke}
                        style={{ display: a.isActive ? undefined : "none" }}
                        initial={false}
                        animate={{
                          strokeDasharray: `${a.visible} ${100 - a.visible}`,
                          strokeDashoffset: a.offset,
                          opacity: dimmed ? 0.28 : 1,
                          strokeWidth: hovered === a.source.id ? 14 : 11,
                        }}
                        transition={arcTransition}
                      />
                    );
                  })}
                </g>
              </svg>

              {/* center readout */}
              <div className="pointer-events-none absolute inset-0 flex flex-col items-center justify-center text-center">
                <AnimatePresence mode="wait">
                  {hoveredSource ? (
                    <motion.div
                      key={`h-${hoveredSource.id}`}
                      initial={{ opacity: 0, y: 5 }}
                      animate={{ opacity: 1, y: 0 }}
                      exit={{ opacity: 0, y: -5 }}
                      transition={{ duration: reduced ? 0 : 0.18 }}
                      className="px-6"
                    >
                      <span className={`text-[0.72rem] font-semibold uppercase tracking-wide ${hoveredSource.color.text}`}>
                        {hoveredSource.label}
                      </span>
                      <p className="mt-0.5 text-2xl font-semibold tabular-nums text-slate-900 dark:text-slate-50">
                        {fmt(hoveredSource.value, 0)}
                      </p>
                      <p className="text-xs text-slate-500 dark:text-slate-400">
                        {hoveredActive ? `${hoveredPct.toFixed(1)}% of mix` : "Hidden from chart"}
                      </p>
                    </motion.div>
                  ) : (
                    <motion.div
                      key="total"
                      initial={{ opacity: 0, y: 5 }}
                      animate={{ opacity: 1, y: 0 }}
                      exit={{ opacity: 0, y: -5 }}
                      transition={{ duration: reduced ? 0 : 0.18 }}
                    >
                      <p className="text-3xl font-semibold tracking-tight tabular-nums text-slate-900 sm:text-4xl dark:text-slate-50">
                        <AnimatedNumber value={total} reduced={reduced} />
                      </p>
                      <p className="mt-1 text-[0.72rem] font-medium uppercase tracking-[0.12em] text-slate-500 dark:text-slate-400">
                        Total sessions
                      </p>
                    </motion.div>
                  )}
                </AnimatePresence>
              </div>
            </div>
          </div>

          {/* legend / controls */}
          <div className="flex flex-col justify-center">
            <div className="mb-3 flex items-baseline justify-between">
              <h3 className="text-sm font-semibold text-slate-700 dark:text-slate-200">
                Traffic by channel
              </h3>
              <span className="text-[0.72rem] text-slate-400 dark:text-slate-500">
                {RANGES.find((r) => r.id === range)?.full}
              </span>
            </div>

            <ul className="flex flex-col gap-1">
              {sources.map((s) => {
                const isActive = activeIds.includes(s.id);
                const pct = isActive && total > 0 ? (s.value / total) * 100 : 0;
                const isLast = isActive && active.length === 1;
                return (
                  <li key={s.id}>
                    <button
                      type="button"
                      aria-pressed={isActive}
                      aria-disabled={isLast}
                      title={isLast ? "Keep at least one channel active" : undefined}
                      onClick={() => toggle(s.id)}
                      onMouseEnter={() => setHovered(s.id)}
                      onMouseLeave={() => setHovered((h) => (h === s.id ? null : h))}
                      onFocus={() => setHovered(s.id)}
                      onBlur={() => setHovered((h) => (h === s.id ? null : h))}
                      className={[
                        "group flex w-full items-center gap-3 rounded-xl px-3 py-2.5 text-left transition-colors",
                        "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900",
                        "hover:bg-slate-100/80 dark:hover:bg-slate-800/60",
                        isLast ? "cursor-not-allowed" : "cursor-pointer",
                      ].join(" ")}
                    >
                      <span
                        className={[
                          "flex h-4 w-4 shrink-0 items-center justify-center rounded-[5px] border-2 transition-all",
                          isActive
                            ? `${s.color.bg} border-transparent`
                            : "border-slate-300 bg-transparent dark:border-slate-600",
                        ].join(" ")}
                        aria-hidden="true"
                      >
                        {isActive && (
                          <svg viewBox="0 0 10 10" width="9" height="9" className="text-white">
                            <path
                              d="M1.5 5.2 4 7.5 8.5 2.5"
                              fill="none"
                              stroke="currentColor"
                              strokeWidth="1.6"
                              strokeLinecap="round"
                              strokeLinejoin="round"
                            />
                          </svg>
                        )}
                      </span>

                      <span className="min-w-0 flex-1">
                        <span className="flex items-center justify-between gap-2">
                          <span
                            className={[
                              "truncate text-sm font-medium transition-colors",
                              isActive
                                ? "text-slate-800 dark:text-slate-100"
                                : "text-slate-400 line-through dark:text-slate-500",
                            ].join(" ")}
                          >
                            {s.label}
                          </span>
                          <span className="shrink-0 text-sm font-semibold tabular-nums text-slate-700 dark:text-slate-200">
                            {fmt(s.value, 0)}
                          </span>
                        </span>

                        <span className="mt-1.5 flex items-center gap-2">
                          <span className="relative h-1.5 flex-1 overflow-hidden rounded-full bg-slate-200 dark:bg-slate-700">
                            <motion.span
                              className={`absolute inset-y-0 left-0 rounded-full ${s.color.bg}`}
                              initial={false}
                              animate={{ width: `${pct}%`, opacity: isActive ? 1 : 0.3 }}
                              transition={{ duration: reduced ? 0 : 0.6, ease: EASE }}
                            />
                          </span>
                          <span className="w-10 shrink-0 text-right text-[0.72rem] tabular-nums text-slate-400 dark:text-slate-500">
                            {isActive ? `${pct.toFixed(1)}%` : "—"}
                          </span>
                        </span>
                      </span>
                    </button>
                  </li>
                );
              })}
            </ul>

            <p className="mt-3 px-3 text-[0.72rem] leading-relaxed text-slate-400 dark:text-slate-500">
              Percentages reflect the active mix. Keyboard: Tab to a channel, Space to toggle it
              in or out of the chart.
            </p>
          </div>
        </div>

        {/* ------------------------------ KPI grid ----------------------------- */}
        <div className="mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
          {data.kpis.map((kpi, i) => (
            <KpiCard key={kpi.id} kpi={kpi} index={i} reduced={reduced} />
          ))}
        </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

Icons Stat

Icons Stat

Original

icon stat grid