Dashboard Users
Original · freeactive users widget
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/dash-users.json"use client";
import { useCallback, useEffect, useId, useMemo, useRef, useState, useSyncExternalStore } from "react";
import type { CSSProperties, KeyboardEvent } from "react";
import { motion, useReducedMotion } from "motion/react";
const REDUCE_QUERY = "(prefers-reduced-motion: reduce)";
function subscribeReduced(onChange: () => void): () => void {
const mq = window.matchMedia(REDUCE_QUERY);
mq.addEventListener("change", onChange);
return () => mq.removeEventListener("change", onChange);
}
function getReducedSnapshot(): boolean {
return window.matchMedia(REDUCE_QUERY).matches;
}
function getReducedServerSnapshot(): boolean {
return false;
}
type WindowId = "5m" | "30m" | "24h";
type Unit = "s" | "m" | "h";
type Point = { id: number; v: number };
type Series = Record<WindowId, Point[]>;
type BarStyle = CSSProperties & Record<`--${string}`, string | number>;
type WindowDef = {
id: WindowId;
name: string;
short: string;
buckets: number;
step: number;
unit: Unit;
base: number;
amp: number;
caption: string;
avgLabel: string;
};
const WINDOWS: readonly WindowDef[] = [
{
id: "5m",
name: "Last 5 minutes",
short: "5 min",
buckets: 30,
step: 10,
unit: "s",
base: 980,
amp: 150,
caption: "Concurrent users, sampled every 10 seconds",
avgLabel: "5-min average",
},
{
id: "30m",
name: "Last 30 minutes",
short: "30 min",
buckets: 30,
step: 1,
unit: "m",
base: 910,
amp: 300,
caption: "Concurrent users, sampled every minute",
avgLabel: "30-min average",
},
{
id: "24h",
name: "Last 24 hours",
short: "24 h",
buckets: 24,
step: 1,
unit: "h",
base: 720,
amp: 560,
caption: "Concurrent users, hourly high-water mark",
avgLabel: "24-hour average",
},
];
const CHANNELS: readonly { label: string; weight: number; bar: string; dot: string }[] = [
{ label: "Organic search", weight: 0.4, bar: "bg-indigo-500", dot: "bg-indigo-500" },
{ label: "Direct", weight: 0.26, bar: "bg-violet-500", dot: "bg-violet-500" },
{ label: "Referral", weight: 0.16, bar: "bg-sky-500", dot: "bg-sky-500" },
{ label: "Social", weight: 0.11, bar: "bg-emerald-500", dot: "bg-emerald-500" },
{ label: "Email", weight: 0.07, bar: "bg-amber-500", dot: "bg-amber-500" },
];
const PAGES: readonly { path: string; tag: string; weight: number }[] = [
{ path: "/pricing", tag: "Marketing", weight: 0.21 },
{ path: "/docs/quickstart", tag: "Docs", weight: 0.17 },
{ path: "/app/projects", tag: "Product", weight: 0.15 },
{ path: "/blog/edge-cache-invalidation", tag: "Blog", weight: 0.11 },
{ path: "/changelog", tag: "Marketing", weight: 0.07 },
];
function mulberry32(seed: number): () => number {
let a = seed >>> 0;
return () => {
a = (a + 0x6d2b79f5) >>> 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
function hash01(seed: number, k: number): number {
const x = Math.sin(seed * 12.9898 + k * 78.233) * 43758.5453;
return x - Math.floor(x);
}
function makeSeries(seed: number, w: WindowDef): Point[] {
const rnd = mulberry32(seed);
const out: Point[] = [];
let drift = 0;
for (let i = 0; i < w.buckets; i++) {
drift += (rnd() - 0.47) * w.amp * 0.16;
const wave = Math.sin((i / w.buckets) * Math.PI * 2.1) * w.amp * 0.4;
const v = Math.round(w.base + wave + drift + (rnd() - 0.5) * w.amp * 0.24);
out.push({ id: i, v: Math.max(80, v) });
}
return out;
}
function advance(pts: Point[], w: WindowDef, rnd: () => number): Point[] {
const last = pts[pts.length - 1] ?? { id: 0, v: w.base };
const pull = (w.base - last.v) * 0.09;
const noise = (rnd() - 0.5) * w.amp * 0.18;
const v = Math.max(80, Math.round(last.v + pull + noise));
return [...pts.slice(1), { id: last.id + 1, v }];
}
function jitterWeights(weights: readonly number[], seed: number, salt: number): number[] {
return weights.map((wt, k) => wt * (0.84 + 0.32 * hash01(seed + salt, k)));
}
function agoLabel(fromEnd: number, w: WindowDef): string {
if (fromEnd <= 0) return "now";
const n = fromEnd * w.step;
if (w.unit === "s") {
if (n < 60) return `${n}s ago`;
const m = Math.floor(n / 60);
const s = n % 60;
return s === 0 ? `${m}m ago` : `${m}m ${s}s ago`;
}
if (w.unit === "m") return n === 1 ? "1 min ago" : `${n} min ago`;
return n === 1 ? "1 h ago" : `${n} h ago`;
}
function fmt(n: number): string {
return n.toLocaleString("en-US");
}
function PauseIcon() {
return (
<svg viewBox="0 0 16 16" aria-hidden="true" className="h-3.5 w-3.5" fill="currentColor">
<rect x="4" y="3" width="3" height="10" rx="1" />
<rect x="9" y="3" width="3" height="10" rx="1" />
</svg>
);
}
function PlayIcon() {
return (
<svg viewBox="0 0 16 16" aria-hidden="true" className="h-3.5 w-3.5" fill="currentColor">
<path d="M5 3.6c0-.8.9-1.3 1.5-.8l5.6 3.9c.6.4.6 1.3 0 1.7l-5.6 3.9c-.7.5-1.5 0-1.5-.8V3.6Z" />
</svg>
);
}
export default function DashUsers() {
const reduce = useReducedMotion();
const groupLabelId = useId();
const chartLabelId = useId();
const [windowId, setWindowId] = useState<WindowId>("5m");
const [liveOverride, setLiveOverride] = useState<boolean | null>(null);
const [hoverIdx, setHoverIdx] = useState<number | null>(null);
const [focusIdx, setFocusIdx] = useState<number | null>(null);
const [rovingIdx, setRovingIdx] = useState<number | null>(null);
const [status, setStatus] = useState("Live user count updating every 2 seconds.");
const [series, setSeries] = useState<Series>(() => ({
"5m": makeSeries(20260717, WINDOWS[0]),
"30m": makeSeries(41220913, WINDOWS[1]),
"24h": makeSeries(70431188, WINDOWS[2]),
}));
const rndRef = useRef<(() => number) | null>(null);
const barsRef = useRef<Array<HTMLButtonElement | null>>([]);
const tabRefs = useRef<Array<HTMLButtonElement | null>>([]);
const rnd = useCallback(() => {
if (!rndRef.current) rndRef.current = mulberry32(90210457);
return rndRef.current();
}, []);
// Motion-sensitive visitors start paused; an explicit choice always wins.
// This drives rendered markup, so it can't read useReducedMotion() directly: that
// resolves to true on the client's first paint but false during SSR, which
// hydration-mismatches the pill. useSyncExternalStore hands React a server
// snapshot for the first render, then settles on the real value.
const reducedDefault = useSyncExternalStore(subscribeReduced, getReducedSnapshot, getReducedServerSnapshot);
const live = liveOverride ?? !reducedDefault;
useEffect(() => {
if (!live) return;
const t = window.setInterval(() => {
setSeries((prev) => ({
"5m": advance(prev["5m"], WINDOWS[0], rnd),
"30m": advance(prev["30m"], WINDOWS[1], rnd),
"24h": advance(prev["24h"], WINDOWS[2], rnd),
}));
}, 2000);
return () => window.clearInterval(t);
}, [live, rnd]);
const w = WINDOWS.find((x) => x.id === windowId) ?? WINDOWS[0];
const pts = series[windowId];
const n = pts.length;
const { nowV, avg, peak, floor, ceil } = useMemo(() => {
const total = pts.reduce((a, p) => a + p.v, 0);
const mean = total / Math.max(1, pts.length);
const hi = pts.reduce((a, p) => Math.max(a, p.v), 0);
const lo = pts.reduce((a, p) => Math.min(a, p.v), Number.POSITIVE_INFINITY);
// Concurrency never drops near zero, so a zero baseline flattens every bar into
// one wall. Frame the band around the data instead and keep some headroom.
const spread = Math.max(1, hi - lo);
return {
nowV: pts[pts.length - 1]?.v ?? 0,
avg: mean,
peak: hi,
floor: Math.max(0, lo - spread * 0.75),
ceil: hi + spread * 0.18,
};
}, [pts]);
const pctOf = useCallback(
(v: number) => {
const span = Math.max(1, ceil - floor);
return Math.min(100, Math.max(2, ((v - floor) / span) * 100));
},
[floor, ceil],
);
const deltaPct = avg > 0 ? ((nowV - avg) / avg) * 100 : 0;
const up = deltaPct >= 0;
const probing = focusIdx !== null || hoverIdx !== null;
const activeIdx = Math.min(n - 1, focusIdx ?? hoverIdx ?? n - 1);
const activePoint = pts[activeIdx] ?? pts[n - 1];
const activeCount = activePoint?.v ?? 0;
const activeId = activePoint?.id ?? 0;
const activeAgo = agoLabel(n - 1 - activeIdx, w);
const channels = useMemo(() => {
const raw = jitterWeights(
CHANNELS.map((c) => c.weight),
activeId,
11,
);
const sum = raw.reduce((a, b) => a + b, 0) || 1;
return CHANNELS.map((c, k) => {
const share = (raw[k] ?? 0) / sum;
return { ...c, share, count: Math.round(share * activeCount) };
});
}, [activeId, activeCount]);
const pages = useMemo(() => {
const raw = jitterWeights(
PAGES.map((p) => p.weight),
activeId,
29,
);
const used = raw.reduce((a, b) => a + b, 0);
const top = Math.max(...raw, 0.0001);
const rows = PAGES.map((p, k) => {
const share = raw[k] ?? 0;
return { ...p, share, rel: (share / top) * 100, count: Math.round(share * activeCount) };
});
const otherShare = Math.max(0.05, 1 - used);
return { rows, otherCount: Math.round(otherShare * activeCount) };
}, [activeId, activeCount]);
const roving = Math.min(rovingIdx ?? n - 1, n - 1);
const guidePct = ((activeIdx + 0.5) / n) * 100;
const chipPct = Math.min(90, Math.max(10, guidePct));
const spring = reduce ? { duration: 0 } : { type: "spring" as const, stiffness: 210, damping: 26, mass: 0.6 };
function selectWindow(id: WindowId) {
setWindowId(id);
setHoverIdx(null);
setFocusIdx(null);
setRovingIdx(null);
const next = WINDOWS.find((x) => x.id === id);
setStatus(next ? `Showing ${next.name.toLowerCase()}.` : "Time window changed.");
}
function onTabsKey(e: KeyboardEvent<HTMLDivElement>) {
const i = WINDOWS.findIndex((x) => x.id === windowId);
let next = i;
if (e.key === "ArrowRight" || e.key === "ArrowDown") next = (i + 1) % WINDOWS.length;
else if (e.key === "ArrowLeft" || e.key === "ArrowUp") next = (i - 1 + WINDOWS.length) % WINDOWS.length;
else if (e.key === "Home") next = 0;
else if (e.key === "End") next = WINDOWS.length - 1;
else return;
e.preventDefault();
const target = WINDOWS[next];
if (!target) return;
selectWindow(target.id);
tabRefs.current[next]?.focus();
}
function onBarsKey(e: KeyboardEvent<HTMLDivElement>) {
let next = roving;
if (e.key === "ArrowRight") next = Math.min(n - 1, roving + 1);
else if (e.key === "ArrowLeft") next = Math.max(0, roving - 1);
else if (e.key === "Home") next = 0;
else if (e.key === "End") next = n - 1;
else if (e.key === "Escape") {
e.preventDefault();
barsRef.current[roving]?.blur();
setFocusIdx(null);
setStatus("Left the chart. Showing the live count.");
return;
} else return;
e.preventDefault();
setRovingIdx(next);
barsRef.current[next]?.focus();
}
function toggleLive() {
const nextLive = !live;
setLiveOverride(nextLive);
setStatus(nextLive ? "Live updates resumed." : `Live updates paused at ${fmt(nowV)} active users.`);
}
return (
<section className="relative w-full overflow-hidden bg-slate-50 px-4 py-16 sm:px-6 sm:py-20 dark:bg-slate-950">
<style>{`
@keyframes dau-ping {
0% { transform: scale(1); opacity: .5; }
70% { transform: scale(2.6); opacity: 0; }
100% { transform: scale(2.6); opacity: 0; }
}
@keyframes dau-rise {
from { transform: scaleY(.12); opacity: 0; }
to { transform: scaleY(1); opacity: 1; }
}
@keyframes dau-breathe {
0%, 100% { opacity: .3; }
50% { opacity: .65; }
}
.dau-bar {
transform-origin: bottom;
animation: dau-rise .55s cubic-bezier(.2,.85,.25,1) both;
animation-delay: var(--dau-d, 0ms);
}
.dau-ping { animation: dau-ping 1.9s cubic-bezier(0,0,.2,1) infinite; }
.dau-breathe { animation: dau-breathe 5s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
.dau-bar, .dau-ping, .dau-breathe { animation: none !important; }
.dau-bar { opacity: 1; transform: none; }
}
`}</style>
<div
aria-hidden="true"
className="dau-breathe pointer-events-none absolute -top-28 left-1/2 h-72 w-[42rem] -translate-x-1/2 rounded-full bg-indigo-300/40 blur-3xl dark:bg-indigo-600/20"
/>
<div className="relative mx-auto w-full max-w-4xl">
<p className="sr-only" role="status" aria-live="polite">
{status}
</p>
<div className="rounded-3xl border border-slate-200 bg-white p-5 shadow-sm sm:p-7 dark:border-slate-800 dark:bg-slate-900">
{/* Header */}
<div className="flex flex-wrap items-start justify-between gap-4">
<div className="flex items-start gap-3">
<span className="mt-0.5 grid h-10 w-10 shrink-0 place-items-center rounded-xl bg-indigo-50 text-indigo-600 ring-1 ring-indigo-100 dark:bg-indigo-500/10 dark:text-indigo-300 dark:ring-indigo-500/20">
<svg viewBox="0 0 24 24" aria-hidden="true" className="h-5 w-5" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 11a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7Z" />
<path d="M2.5 19.5a6.5 6.5 0 0 1 13 0" />
<path d="M16.5 4.6a3.5 3.5 0 0 1 0 6.8" />
<path d="M18 14.4a6.5 6.5 0 0 1 3.5 5.1" />
</svg>
</span>
<div>
<div className="flex items-center gap-2">
<h2 className="text-sm font-semibold tracking-tight text-slate-900 dark:text-slate-100">
Active users right now
</h2>
<span
className={`inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-[11px] font-medium ring-1 ${
live
? "bg-emerald-50 text-emerald-700 ring-emerald-200 dark:bg-emerald-500/10 dark:text-emerald-300 dark:ring-emerald-500/25"
: "bg-amber-50 text-amber-700 ring-amber-200 dark:bg-amber-500/10 dark:text-amber-300 dark:ring-amber-500/25"
}`}
>
<span className="relative flex h-1.5 w-1.5">
{live ? (
<span className="dau-ping absolute inline-flex h-full w-full rounded-full bg-emerald-500" />
) : null}
<span className={`relative inline-flex h-1.5 w-1.5 rounded-full ${live ? "bg-emerald-500" : "bg-amber-500"}`} />
</span>
{live ? "Live" : "Paused"}
</span>
</div>
<p className="mt-0.5 text-xs text-slate-500 dark:text-slate-400">
{w.caption}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<div
role="radiogroup"
aria-labelledby={groupLabelId}
onKeyDown={onTabsKey}
className="flex rounded-lg bg-slate-100 p-0.5 dark:bg-slate-800"
>
<span id={groupLabelId} className="sr-only">
Time window
</span>
{WINDOWS.map((x, i) => {
const on = x.id === windowId;
return (
<button
key={x.id}
ref={(el) => {
tabRefs.current[i] = el;
}}
type="button"
role="radio"
aria-checked={on}
aria-label={x.name}
tabIndex={on ? 0 : -1}
onClick={() => selectWindow(x.id)}
className={`rounded-[7px] px-2.5 py-1 text-xs 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-white dark:focus-visible:ring-offset-slate-900 ${
on
? "bg-white text-slate-900 shadow-sm dark:bg-slate-700 dark:text-slate-50"
: "text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-slate-200"
}`}
>
{x.short}
</button>
);
})}
</div>
<button
type="button"
onClick={toggleLive}
aria-label={live ? "Pause live updates" : "Resume live updates"}
className="inline-flex items-center gap-1.5 rounded-lg border border-slate-200 bg-white px-2.5 py-1.5 text-xs font-medium text-slate-700 transition-colors hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-slate-700 dark:bg-slate-800 dark:text-slate-200 dark:hover:bg-slate-700 dark:focus-visible:ring-offset-slate-900"
>
{live ? <PauseIcon /> : <PlayIcon />}
{live ? "Pause" : "Resume"}
</button>
</div>
</div>
{/* Headline metric */}
<div className="mt-5 flex flex-wrap items-end gap-x-4 gap-y-2">
<span className="text-5xl font-semibold tracking-tighter tabular-nums text-slate-900 sm:text-6xl dark:text-slate-50">
{fmt(nowV)}
</span>
<span
className={`mb-2 inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-semibold tabular-nums ring-1 ${
up
? "bg-emerald-50 text-emerald-700 ring-emerald-200 dark:bg-emerald-500/10 dark:text-emerald-300 dark:ring-emerald-500/25"
: "bg-rose-50 text-rose-700 ring-rose-200 dark:bg-rose-500/10 dark:text-rose-300 dark:ring-rose-500/25"
}`}
>
<svg viewBox="0 0 12 12" aria-hidden="true" className={`h-3 w-3 ${up ? "" : "rotate-180"}`} fill="currentColor">
<path d="M6 2.2 10.2 7H7.4v3H4.6V7H1.8L6 2.2Z" />
</svg>
{up ? "+" : "−"}
{Math.abs(deltaPct).toFixed(1)}%
</span>
<span className="mb-2 text-xs text-slate-500 dark:text-slate-400">
vs. {w.avgLabel} of {fmt(Math.round(avg))} · peak {fmt(peak)}
</span>
</div>
{/* Chart */}
<div className="relative mt-4 pt-9">
<span id={chartLabelId} className="sr-only">
Active users, {w.name.toLowerCase()}. Use the arrow keys to inspect each sample.
</span>
<div className="flex items-stretch gap-2">
{/* The bars row is the positioning context, so every overlay shares the bars' scale. */}
<div
role="group"
aria-labelledby={chartLabelId}
onKeyDown={onBarsKey}
onMouseLeave={() => setHoverIdx(null)}
className="relative flex h-40 flex-1 items-end gap-[3px] sm:h-48"
>
<motion.div
aria-hidden="true"
className="pointer-events-none absolute -top-9 z-10 -translate-x-1/2"
initial={false}
animate={{ left: `${chipPct}%` }}
transition={spring}
>
<span className="inline-flex items-center gap-1.5 whitespace-nowrap rounded-full border border-slate-200 bg-white px-2.5 py-1 text-[11px] shadow-sm dark:border-slate-700 dark:bg-slate-800">
<span className="font-semibold tabular-nums text-slate-900 dark:text-slate-100">
{fmt(activeCount)}
</span>
<span className="text-slate-400 dark:text-slate-500">·</span>
<span className="text-slate-500 dark:text-slate-400">{activeAgo}</span>
</span>
</motion.div>
<div
aria-hidden="true"
className="pointer-events-none absolute inset-x-0 border-t border-dashed border-slate-300 dark:border-slate-700"
style={{ bottom: `${pctOf(avg)}%` }}
/>
{pts.map((p, i) => {
const h = pctOf(p.v);
const on = i === activeIdx;
const label = agoLabel(n - 1 - i, w);
const barStyle: BarStyle = { "--dau-d": `${i * 16}ms`, height: `${h}%` };
return (
<button
key={i}
ref={(el) => {
barsRef.current[i] = el;
}}
type="button"
tabIndex={i === roving ? 0 : -1}
aria-label={`${label}: ${fmt(p.v)} active users`}
onFocus={() => {
setFocusIdx(i);
setRovingIdx(i);
}}
onBlur={() => setFocusIdx(null)}
onMouseEnter={() => setHoverIdx(i)}
onClick={() => setStatus(`${label}: ${fmt(p.v)} active users.`)}
className="relative flex h-full flex-1 items-end rounded-md 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"
>
<motion.span
aria-hidden="true"
className={`dau-bar w-full rounded-t-[3px] bg-gradient-to-t transition-colors ${
on
? "from-indigo-600 to-violet-400 dark:from-indigo-400 dark:to-violet-300"
: "from-indigo-500/55 to-violet-400/45 dark:from-indigo-500/50 dark:to-violet-400/40"
}`}
style={barStyle}
initial={false}
animate={{ height: `${h}%` }}
transition={spring}
/>
</button>
);
})}
{/* Rendered after the bars so it paints over them. */}
<motion.div
aria-hidden="true"
className={`pointer-events-none absolute inset-y-0 w-px ${
probing ? "bg-indigo-600/50 dark:bg-indigo-300/50" : "bg-slate-900/15 dark:bg-white/15"
}`}
initial={false}
animate={{ left: `${guidePct}%` }}
transition={spring}
/>
</div>
{/* Axis gutter: keeps the reference label off the bars while sharing their scale. */}
<div aria-hidden="true" className="relative w-10 shrink-0">
<span
className="absolute right-0 -translate-y-1/2 whitespace-nowrap text-[10px] font-medium tabular-nums text-slate-400 dark:text-slate-500"
style={{ bottom: `${pctOf(avg)}%` }}
>
avg {fmt(Math.round(avg))}
</span>
</div>
</div>
<div className="mt-2 flex items-center justify-between pr-12 text-[10px] font-medium text-slate-400 dark:text-slate-500">
<span>{agoLabel(n - 1, w)}</span>
<span>now</span>
</div>
</div>
{/* Breakdown */}
<div className="mt-6 grid gap-4 border-t border-slate-200 pt-6 md:grid-cols-2 dark:border-slate-800">
<section aria-label="Traffic sources">
<div className="flex items-baseline justify-between gap-2">
<h3 className="text-xs font-semibold text-slate-900 dark:text-slate-100">Where they came from</h3>
<span className="text-[11px] tabular-nums text-slate-400 dark:text-slate-500">
{probing ? activeAgo : "live"}
</span>
</div>
<div className="mt-3 flex h-2 overflow-hidden rounded-full bg-slate-100 dark:bg-slate-800">
{channels.map((c) => (
<motion.span
key={c.label}
aria-hidden="true"
className={c.bar}
initial={false}
animate={{ width: `${c.share * 100}%` }}
transition={spring}
/>
))}
</div>
<ul className="mt-3 space-y-2">
{channels.map((c) => (
<li key={c.label} className="flex items-center gap-2.5 text-xs">
<span aria-hidden="true" className={`h-2 w-2 shrink-0 rounded-full ${c.dot}`} />
<span className="text-slate-600 dark:text-slate-300">{c.label}</span>
<span className="ml-auto font-medium tabular-nums text-slate-900 dark:text-slate-100">
{fmt(c.count)}
</span>
<span className="w-10 text-right tabular-nums text-slate-400 dark:text-slate-500">
{(c.share * 100).toFixed(0)}%
</span>
</li>
))}
</ul>
</section>
<section aria-label="Top pages">
<div className="flex items-baseline justify-between gap-2">
<h3 className="text-xs font-semibold text-slate-900 dark:text-slate-100">What they’re looking at</h3>
<span className="text-[11px] text-slate-400 dark:text-slate-500">users</span>
</div>
<ul className="mt-3 space-y-2.5">
{pages.rows.map((p) => (
<li key={p.path}>
<div className="flex items-center gap-2 text-xs">
<span className="truncate font-mono text-[11px] text-slate-700 dark:text-slate-300">{p.path}</span>
<span className="shrink-0 rounded border border-slate-200 px-1 py-px text-[9px] font-medium uppercase tracking-wide text-slate-400 dark:border-slate-700 dark:text-slate-500">
{p.tag}
</span>
<span className="ml-auto shrink-0 font-medium tabular-nums text-slate-900 dark:text-slate-100">
{fmt(p.count)}
</span>
</div>
<div className="mt-1 h-1 overflow-hidden rounded-full bg-slate-100 dark:bg-slate-800">
<motion.span
aria-hidden="true"
className="block h-full rounded-full bg-gradient-to-r from-indigo-500 to-violet-400"
initial={false}
animate={{ width: `${p.rel}%` }}
transition={spring}
/>
</div>
</li>
))}
</ul>
<p className="mt-3 text-[11px] text-slate-400 dark:text-slate-500">
+ 96 other pages · <span className="tabular-nums">{fmt(pages.otherCount)}</span> users
</p>
</section>
</div>
<p className="mt-6 border-t border-slate-200 pt-4 text-[11px] leading-relaxed text-slate-400 dark:border-slate-800 dark:text-slate-500">
Counted at the edge, refreshed every 2 seconds. A session drops out after 5 minutes without a request, so
this number trails a hard refresh by a few seconds.
</p>
</div>
</div>
</section>
);
}Dependencies
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 quoteSimilar components
Browse all →
Dashboard KPI Row
Originalrow of KPI stat cards with trends

Dashboard Stat Widgets
Originalassorted dashboard stat widgets

Dashboard Activity
Originalrecent activity feed widget

Dashboard Chart Widget
Originalchart panel widget with tabs

Dashboard Revenue
Originalrevenue overview card with chart

Dashboard Tasks
Originaltasks/checklist widget

Dashboard Calendar Widget
Originalmini calendar dashboard widget

Dashboard Notifications
Originalnotifications panel widget

Dashboard Overview
Originalcompact dashboard overview grid

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

Split Hero
OriginalA two-column hero pairing a headline and CTAs with a product mock and social proof.

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

