ComponentsCSS
Bar List
Ranked rows with a proportional fill behind each label, for top pages, sources or countries. Values are always printed.
Top sources
- Google18,342
- Direct9,417
- news.ycombinator.com4,106
- Newsletter2,251
- LinkedIn1,189
Countries
- DEGermany31%
- TRTürkiye22%
- USUnited States17%
- NLNetherlands9%
Installation
npx shadcn@latest add https://ui-collections.vercel.app/r/tide/bar-list.jsonAlso installs the theme, cn() and use-in-view-once. Packages: clsx, tailwind-merge.
Usage
bar-list-demo.tsx
"use client";
import { BarList } from "@/registry/kit/ui/bar-list";
export default function BarListDemo() {
return (
<div className="grid w-full max-w-3xl gap-4 md:grid-cols-2">
<section className="surface rounded-card p-5">
<h3 className="mb-3 text-sm font-medium">Top sources</h3>
<BarList
label="Top sources, last 30 days"
header={{ label: "Source", value: "Visitors" }}
items={[
{ label: "Google", value: 18342 },
{ label: "Direct", value: 9417 },
{ label: "news.ycombinator.com", value: 4106 },
{ label: "Newsletter", value: 2251 },
{ label: "LinkedIn", value: 1189 },
]}
/>
</section>
<section className="surface rounded-card p-5">
<h3 className="mb-3 text-sm font-medium">Countries</h3>
<BarList
label="Visitors by country, last 30 days"
header={{ label: "Country", value: "Share" }}
formatValue={(v) => `${v}%`}
items={[
{ label: "Germany", value: 31, leading: <span className="font-mono text-[10px] text-muted">DE</span> },
{ label: "Türkiye", value: 22, leading: <span className="font-mono text-[10px] text-muted">TR</span> },
{ label: "United States", value: 17, leading: <span className="font-mono text-[10px] text-muted">US</span> },
{ label: "Netherlands", value: 9, leading: <span className="font-mono text-[10px] text-muted">NL</span> },
]}
/>
</section>
</div>
);
}API
items{ label; value; leading?; href? }[]Rows, largest first.
labelstringAccessible name for the list.
header{ label; value }Column headings above the rows.
formatValue(value) => stringFormats the printed values.
size= "md"
"sm" | "md"Row height.
Source
components/ui/bar-list.tsx
"use client";
import { useRef } from "react";
import { useInViewOnce } from "@/registry/kit/hooks/use-in-view-once";
import { cn } from "@/registry/kit/lib/utils";
export interface BarListItem {
label: string;
value: number;
/** Small text or icon before the label, e.g. a flag or initials. */
leading?: React.ReactNode;
/** Makes the row a link. */
href?: string;
}
export interface BarListProps {
items: BarListItem[];
/** Accessible name for the list, e.g. "Top pages, last 7 days". */
label: string;
/** Column headings shown above the rows. */
header?: { label: string; value: string };
formatValue?: (value: number) => string;
/** Row height. */
size?: "sm" | "md";
className?: string;
}
/**
* Ranked rows with a proportional fill behind each label: top pages, sources,
* countries. The fills grow in when first seen. Values are always printed, so
* the bar is never the only signal.
*/
export function BarList({ items, label, header, formatValue = (v) => v.toLocaleString("en-US"), size = "md", className }: BarListProps) {
const ref = useRef<HTMLDivElement>(null);
const shown = useInViewOnce(ref);
const max = Math.max(...items.map((i) => i.value), 1);
return (
<div ref={ref} className={cn("min-w-0 text-sm", className)}>
{header && (
<div aria-hidden className="mb-1.5 flex justify-between gap-3 px-2 text-xs text-muted">
<span>{header.label}</span>
<span>{header.value}</span>
</div>
)}
<ul aria-label={label} className="grid gap-1">
{items.map((item, i) => {
const row = (
<>
<span
aria-hidden
className="absolute inset-y-0 left-0 origin-left rounded-[max(0.125rem,calc(var(--r-field)-0.25rem))] bg-accent/14 transition-[scale] duration-700 ease-theme"
style={{ width: `${(item.value / max) * 100}%`, scale: shown ? "1 1" : "0 1", transitionDelay: `${i * 60}ms` }}
/>
<span className="relative flex min-w-0 items-center gap-2">
{item.leading}
<span className="truncate">{item.label}</span>
</span>
<span className="relative shrink-0 text-fg/80 tabular-nums">{formatValue(item.value)}</span>
</>
);
const rowClass = cn(
"relative flex items-center justify-between gap-3 rounded-[max(0.125rem,calc(var(--r-field)-0.25rem))] px-2",
size === "sm" ? "h-7 text-xs" : "h-8",
);
return (
<li key={item.label}>
{item.href ? (
<a
href={item.href}
className={cn(rowClass, "outline-none hover:text-accent focus-visible:outline-2 focus-visible:outline-accent")}
>
{row}
</a>
) : (
<div className={rowClass}>{row}</div>
)}
</li>
);
})}
</ul>
</div>
);
}