Ranked horizontal bars for top-pages and referrer panels — soft background fills scale in with a stagger and hover reveals exact values.
npx shadcn@latest add @paragon/mini-bar-list"use client";
import * as React from "react";
import { useInView, useReducedMotion } from "motion/react";
import { cn } from "@/lib/utils";
const compactFormat = new Intl.NumberFormat("en-US", {
notation: "compact",
maximumFractionDigits: 1,
});
export interface MiniBarListItem {
label: string;
value: number;
}
export interface MiniBarListProps extends React.ComponentProps<"div"> {
/** Rows, already ranked — rendered in the order given. */
items: MiniBarListItem[];
/** Bar tint. */
color?: string;
/** Row height in px. */
barHeight?: number;
/** Show the value column. */
showValues?: boolean;
/** Compact value shown in the fixed value column. */
formatValue?: (value: number) => string;
/** Exact value revealed on row hover / focus in a tooltip. */
formatExactValue?: (value: number) => string;
/** Renders the final state immediately, no scale-in. */
static?: boolean;
}
/**
* Ranked horizontal bars for top-pages / referrers panels — the
* Vercel-analytics look. Widths come from a computed linear scale against the
* largest value; the value column is a fixed tabular column that never
* overlaps the label or bar. Each row's soft background bar scales in from the
* left with a 30ms stagger on first view; hovering or keyboard-focusing a row
* reveals the exact value in a tooltip. Reduced motion renders the final state
* immediately.
*/
export function MiniBarList({
items,
color = "oklch(0.585 0.17 260)",
barHeight = 32,
showValues = true,
formatValue = (v) => compactFormat.format(v),
formatExactValue = (v) => v.toLocaleString("en-US"),
static: isStatic = false,
className,
...props
}: MiniBarListProps) {
const containerRef = React.useRef<HTMLDivElement>(null);
const reducedMotion = useReducedMotion();
const inView = useInView(containerRef, {
once: true,
margin: "0px 0px -24px 0px",
});
const [active, setActive] = React.useState<number | null>(null);
const animate = !isStatic && !reducedMotion;
const drawn = !animate || inView;
const max = Math.max(1, ...items.map((item) => item.value));
// Reserve a fixed value column wide enough for the widest formatted value,
// measured in ch so it stays tabular and never overlaps the bar.
const valueColCh = showValues
? Math.max(
3,
...items.map((item) => formatValue(item.value).length),
)
: 0;
if (items.length === 0) {
return (
<div
ref={containerRef}
data-slot="mini-bar-list"
className={cn(
"flex w-full items-center justify-center rounded-md bg-current/[0.03] py-8 text-sm text-muted-foreground",
className,
)}
{...props}
>
No data
</div>
);
}
return (
<div
ref={containerRef}
role="list"
data-slot="mini-bar-list"
className={cn("flex w-full flex-col gap-1", className)}
{...props}
>
{items.map((item, i) => (
<div
key={item.label}
role="listitem"
tabIndex={0}
onPointerEnter={() => setActive(i)}
onPointerLeave={() => setActive((cur) => (cur === i ? null : cur))}
onFocus={() => setActive(i)}
onBlur={() => setActive((cur) => (cur === i ? null : cur))}
className={cn(
"group relative flex items-center gap-3 rounded-md px-2.5 text-sm outline-none",
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",
)}
style={{ height: barHeight }}
>
<span
aria-hidden
className="absolute inset-y-0 left-0 origin-left rounded-md transition-[opacity] duration-150 ease-out group-hover:opacity-100"
style={{
width: `${(item.value / max) * 100}%`,
background: color,
opacity: active === i ? 0.2 : 0.13,
transform: drawn ? "scaleX(1)" : "scaleX(0)",
transition: animate
? `transform 450ms var(--ease-out) ${i * 30}ms, opacity 150ms var(--ease-out)`
: "opacity 150ms var(--ease-out)",
}}
/>
<span className="relative min-w-0 flex-1 truncate">{item.label}</span>
{showValues && (
<span
className="relative shrink-0 text-right text-muted-foreground tabular-nums"
style={{ width: `${valueColCh}ch` }}
>
{formatValue(item.value)}
</span>
)}
{/* Per-row tooltip with the exact value */}
{active === i && (
<span
role="status"
className="pointer-events-none absolute right-2.5 bottom-full z-10 mb-1 whitespace-nowrap rounded-md bg-primary px-2 py-1 text-[11px] font-medium text-primary-foreground shadow-overlay tabular-nums"
>
{formatExactValue(item.value)}
</span>
)}
</div>
))}
</div>
);
}