dashboardcn
uibar-listsource: ok · hit
Bar List
A ranked list with proportional bars, for top pages, referrers, or countries.
npx shadcn add "https://dashboardcn.com/r/bar-list.json"free
No selection tags yet — empty means no evidence, not lack of support.
Files2 · source cached 10/14/2026, 7:37:20 PM
registry/dashboardcn/ui/bar-list.tsx
import * as React from "react"
import { cn } from "@/lib/utils"
import { formatNumber } from "@/registry/dashboardcn/lib/format"
export interface BarListItem {
name: string
value: number
/** Renders the label as a link. */
href?: string
icon?: React.ReactNode
/** Stable key when names can repeat. */
key?: string
}
export interface BarListProps extends React.ComponentProps<"div"> {
data: BarListItem[]
valueFormatter?: (value: number) => string
sortOrder?: "descending" | "ascending" | "none"
/** Any CSS color. Defaults to chart-1. */
color?: string
/** Show each row's share of the total next to its value. */
showPercentage?: boolean
onItemClick?: (item: BarListItem) => void
}
function BarList({
data,
valueFormatter = (value) => formatNumber(value, { format: "compact" }),
sortOrder = "descending",
color = "var(--chart-1)",
showPercentage = false,
onItemClick,
className,
...props
}: BarListProps) {
const items = React.useMemo(() => {
if (sortOrder === "none") return data
const sorted = [...data].sort((a, b) => a.value - b.value)
return sortOrder === "descending" ? sorted.reverse() : sorted
}, [data, sortOrder])
const max = Math.max(...items.map((item) => item.value), 0)
const total = items.reduce((sum, item) => sum + item.value, 0)
return (
<div
data-slot="bar-list"
className={cn("flex flex-col gap-1.5", className)}
{...props}
>
{items.map((item) => {
const width = max > 0 ? (item.value / max) * 100 : 0
const share = total > 0 ? item.value / total : 0
const interactive = Boolean(item.href || onItemClick)
const Label = item.href ? "a" : onItemClick ? "button" : "div"
return (
<div
key={item.key ?? item.name}
data-slot="bar-list-row"
className="flex items-center gap-3 text-sm"
>
<div className="relative flex h-8 min-w-0 flex-1 items-center">
<div
aria-hidden="true"
className="absolute inset-y-0 left-0 rounded-md transition-[width] duration-300"
style={{
width: `${width}%`,
backgroundColor: `color-mix(in oklab, ${color} 20%, transparent)`,
}}
/>
<Label
{...(item.href ? { href: item.href } : {})}
{...(onItemClick && !item.href
? { type: "button" as const, onClick: () => onItemClick(item) }
: {})}
className={cn(
"relative flex min-w-0 items-center gap-2 px-2 text-left",
interactive && "hover:underline"
)}
>
{item.icon ? (
<span className="text-muted-foreground shrink-0 [&>svg]:size-4">
{item.icon}
</span>
) : null}
<span className="truncate">{item.name}</span>
</Label>
</div>
<div className="flex shrink-0 items-center gap-2 tabular-nums">
<span className="font-medium">{valueFormatter(item.value)}</span>
{showPercentage ? (
<span className="text-muted-foreground w-12 text-right text-xs">
{formatNumber(share, { format: "percent" })}
</span>
) : null}
</div>
</div>
)
})}
</div>
)
}
export { BarList }
registry/dashboardcn/lib/format.ts
export type NumberFormat = "number" | "compact" | "currency" | "percent"
export interface FormatNumberOptions {
format?: NumberFormat
/** ISO 4217 code, used when format is "currency". Defaults to USD. */
currency?: string
locale?: string
maximumFractionDigits?: number
/** Abbreviate large values, e.g. "$158K" instead of "$158,143". */
compact?: boolean
}
const formatterCache = new Map<string, Intl.NumberFormat>()
function getFormatter(locale: string, options: Intl.NumberFormatOptions) {
const key = locale + JSON.stringify(options)
let formatter = formatterCache.get(key)
if (!formatter) {
formatter = new Intl.NumberFormat(locale, options)
formatterCache.set(key, formatter)
}
return formatter
}
/**
* Format a metric value for display.
*
* formatNumber(1234567) -> "1,234,567"
* formatNumber(1234567, { format: "compact" }) -> "1.2M"
* formatNumber(48.2, { format: "currency" }) -> "$48.20"
* formatNumber(158143, { format: "currency", compact: true }) -> "$158K"
* formatNumber(0.124, { format: "percent" }) -> "12.4%"
*/
export function formatNumber(
value: number,
{
format = "number",
currency = "USD",
locale = "en-US",
maximumFractionDigits,
compact = false,
}: FormatNumberOptions = {}
): string {
if (!Number.isFinite(value)) return "—"
// Compact values keep three significant digits: $158K, $1.23M, 41.2K.
const compactOptions: Intl.NumberFormatOptions = compact
? maximumFractionDigits === undefined
? { notation: "compact", maximumSignificantDigits: 3 }
: { notation: "compact", maximumFractionDigits }
: {}
switch (format) {
case "compact":
return getFormatter(locale, {
notation: "compact",
maximumFractionDigits: maximumFractionDigits ?? 1,
}).format(value)
case "currency":
return getFormatter(locale, {
style: "currency",
currency,
maximumFractionDigits: maximumFractionDigits ?? 2,
...compactOptions,
}).format(value)
case "percent":
return getFormatter(locale, {
style: "percent",
maximumFractionDigits: maximumFractionDigits ?? 1,
}).format(value)
default:
return getFormatter(locale, {
maximumFractionDigits: maximumFractionDigits ?? 0,
...compactOptions,
}).format(value)
}
}
/**
* Format a fractional change as a signed percentage.
*
* formatDelta(0.124) -> "+12.4%"
* formatDelta(-0.03) -> "-3.0%"
* formatDelta(0) -> "0.0%"
*/
export function formatDelta(delta: number, locale = "en-US"): string {
if (!Number.isFinite(delta)) return "—"
return getFormatter(locale, {
style: "percent",
signDisplay: "exceptZero",
maximumFractionDigits: 1,
minimumFractionDigits: 1,
}).format(delta)
}
/**
* Fractional change between two values. Returns 0 when there is no baseline.
*/
export function computeDelta(current: number, previous: number): number {
if (!previous) return 0
return (current - previous) / Math.abs(previous)
}