- Accordion
- Agent Thinking
- Alert
- Alert Dialog
- Announcement
- Aspect Ratio
- Attachment
- Avatar
- Badge
- Breadcrumb
- Bubble
- Button
- Button Group
- Calendar
- Card
- Carousel
- Checkbox
- Checkbox Card
- Code Block
- Collapsible
- Color Picker
- Combobox
- Command
- Composer
- Composer Loader
- Context Menu
- Data Grid
- Data Table
- Date Picker
- Dialog
- Direction
- Drawer
- Dropdown Menu
- Empty
- Field
- File Upload
- Hover Card
- Input
- Input Group
- Input Otp
- Item
- Kbd
- Label
- Marker
- Menubar
- Message
- Message Scroller
- Meter
- Native Select
- Navigation Menu
- Number Field
- Pagination
- Period Switcher
- Popover
- Progress
- Questionnaire
- Radio Card
- Radio Group
- Resizable
- Reveal
- Scroll Area
- Search Field
- Select
- Separator
- Sheet
- Sidebar
- Skeleton
- Slider
- Social Button
- Sonner
- Spinner
- Status Dot
- Switch
- Switch Card
- Table
- Tabs
- Tag Group
- Textarea
- Theme Toggle
- Time Field
- Toggle
- Toggle Group
- Tooltip
- Trace
- Tree View
- Agent Creator 01
- Agent Limits Card 01
- Agent Progress 01
- AI Chat 01
- AI Image Generation 01
- AI Profile 01
- Alerts Card 01
- Auth Card 01
- Calendar 01
- Composer 01
- Composer Attachments 01
- Composer Panel 01
- Finance Dashboard 01
- Home Dashboard 01
- Marketing Dashboard 01
- Medical Profile 01
- Meeting Scheduler 01
- Multi Agent Chat 01
- Notification Center 01
- Patient Info Card 01
- Project Board 01
- Questionnaire 01
- Settings Modal 01
- Sidebar 01
- Stat Cards 01
- Task List 01
- Web Search 01
- Activity Rings Card 01
- Area Chart Card 01
- Bar List Card 01
- Combo Chart Card 01
- Contributions Card 01
- Earnings Chart Card 01
- Funnel Chart Card 01
- Heatmap Chart Card 01
- Line Chart Card 01
- Most Active Days Card 01
- Orders Chart Card 01
- Radar Chart Card 01
- Radial Chart Card 01
- Revenue Chart Card 01
- Sankey Chart Card 01
- Scatter Chart Card 01
- Sleep Score Card 01
- Stage Bars Card 01
- Steps Card 01
Sessions
- Laptop54%
- Handheld32%
- Slate14%
SectionsVisits
- /home8,320
- /journal5,270
- /team3,120
- /contact1,940
Sessions
- Laptop54%
- Handheld32%
- Slate14%
Installation
Set up the @cobalt registry first, as described in Installation, then add the block:
$ pnpm dlx shadcn@latest add @cobalt/bar-list-card-01
Pass items and a title for one list, or tabs for multiple lists. Set metric="value" to display counts, mono for neutral bars, and limit to offer an expand control. Bar lengths are relative to the largest item in each list; percentages use the total of that list.
Source
"use client";
import { RiArrowDownSLine } from "@remixicon/react";
import { cn } from "cn";
import * as React from "react";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import {
Collapsible,
CollapsibleContent,
} from "@/components/ui/collapsible";
import { Reveal } from "@/components/ui/reveal";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/components/ui/tabs";
export interface BarListItem {
label: string;
value: number;
icon?: React.ReactNode;
}
export interface BarListTab {
id: string;
label: string;
items: BarListItem[];
}
export interface BarListCardProps {
title?: string;
metricLabel?: string;
metric?: "share" | "value";
items?: BarListItem[];
tabs?: BarListTab[];
defaultTab?: string;
onTabChange?: (id: string) => void;
limit?: number;
mono?: boolean;
className?: string;
}
function BarList({
items,
metric,
limit,
mono,
minHeight,
}: {
items: BarListItem[];
metric: "share" | "value";
limit: number;
mono: boolean;
minHeight?: number;
}) {
const [expanded, setExpanded] = React.useState(false);
const validItems = items.map((item) => ({
...item,
amount: Number.isFinite(item.value) ? Math.max(0, item.value) : 0,
}));
const total = validItems.reduce((sum, item) => sum + item.amount, 0);
const max = Math.max(0, ...validItems.map((item) => item.amount));
const rowLimit = Math.max(1, limit);
const hasMore = validItems.length > rowLimit;
const renderRow = (
item: (typeof validItems)[number],
index: number,
fade: boolean
) => (
<li
key={`${item.label}-${index}`}
className={cn(
"relative flex h-9 min-w-0 items-center justify-between gap-3 overflow-hidden rounded-[10px] px-3 text-sm leading-5 text-foreground",
fade &&
"group-data-closed/collapsible:[mask-image:linear-gradient(to_bottom,black_10%,transparent)]"
)}
>
{/* A full-width fill slid left, so a change only moves it (composited). */}
<span
aria-hidden
className={cn(
"bar-list-card-01-draw absolute inset-0 translate-x-(--bar-translate) rounded-[10px] transition-all duration-450 ease-out motion-reduce:transition-none",
mono
? "bg-foreground/10"
: "bg-primary/15 not-dark:bg-(--badge-primary-background,var(--primary))/15"
)}
style={
{
"--bar-translate": `calc(${max ? (item.amount / max) * 100 : 0}% - 100%)`,
} as React.CSSProperties
}
/>
<span className="relative flex min-w-0 items-center gap-2">
<span className="shrink-0 text-muted-foreground [&_svg]:size-4">
{item.icon}
</span>
<span className="truncate">{item.label}</span>
</span>
<span className="relative shrink-0 font-medium tabular-nums">
{metric === "value"
? item.value.toLocaleString()
: `${total ? Math.round((item.amount / total) * 100) : 0}%`}
</span>
</li>
);
return (
<Collapsible
open={expanded}
onOpenChange={setExpanded}
className="relative min-h-(--bar-list-min-height)"
style={
{
"--bar-list-min-height": `${minHeight ?? 0}px`,
} as React.CSSProperties
}
>
<div className="px-2 py-2">
<ul className="flex flex-col gap-1">
{validItems
.slice(0, rowLimit)
.map((item, index) =>
renderRow(item, index, hasMore && index === rowLimit - 1)
)}
</ul>
{hasMore && (
<CollapsibleContent>
<ul className="flex flex-col gap-1 pt-1">
{validItems
.slice(rowLimit)
.map((item, index) => renderRow(item, index + rowLimit, false))}
</ul>
</CollapsibleContent>
)}
{hasMore && (
<div className="relative mt-1 flex justify-center transition-all duration-250 ease-out group-data-closed/collapsible:-mt-6 motion-reduce:transition-none">
<Button
type="button"
variant="outline"
size="icon-2xs"
shape="pill"
className="w-10"
onClick={() => setExpanded(!expanded)}
aria-expanded={expanded}
tooltip={expanded ? "Show fewer items" : "Show all items"}
>
<RiArrowDownSLine className="transition-transform duration-200 ease-out group-data-open/collapsible:rotate-180 motion-reduce:transition-none" />
</Button>
</div>
)}
</div>
</Collapsible>
);
}
export function BarListCard({
title = "Overview",
metricLabel = "Total",
metric = "share",
items = [],
tabs,
defaultTab,
onTabChange,
limit = 5,
mono = false,
className,
}: BarListCardProps) {
const tabItems = tabs?.filter((tab) => tab.id);
const initialTab = tabItems?.some((tab) => tab.id === defaultTab)
? defaultTab
: tabItems?.[0]?.id;
let tabMinHeight = 0;
const rowLimit = Math.max(1, limit);
for (const tab of tabItems ?? []) {
const rows = Math.min(tab.items.length, rowLimit);
const listHeight = rows * 36 + Math.max(0, rows - 1) * 4;
tabMinHeight = Math.max(
tabMinHeight,
16 + listHeight + (tab.items.length > rowLimit ? 24 : 0)
);
}
return (
<Reveal
variant="card"
render={
<Card
radius={16}
size="flush"
variant="muted"
render={<section aria-label={title} />}
className={cn("min-w-0", className)}
/>
}
>
{tabItems?.length && initialTab ? (
<Tabs
defaultValue={initialTab}
onValueChange={(value) => {
if (value !== null) {
onTabChange?.(value);
}
}}
size="flush"
>
<div className="flex min-w-0 items-center px-2">
<TabsList
variant="line"
aria-label={title}
className="min-w-0 flex-1 overflow-x-auto"
>
{tabItems.map((tab) => (
<TabsTrigger key={tab.id} value={tab.id}>
{tab.label}
</TabsTrigger>
))}
</TabsList>
{/* oxlint-disable-next-line shadcn/no-arbitrary-values -- reference: the metric caption is 11px uppercase with wide tracking in the faint grey */}
<span className="shrink-0 px-2 text-[11px] font-normal tracking-widest text-(--foreground-faint,var(--muted-foreground)) uppercase">
{metricLabel}
</span>
</div>
{tabItems.map((tab) => (
<TabsContent key={tab.id} value={tab.id}>
<BarList
items={tab.items}
metric={metric}
limit={limit}
mono={mono}
minHeight={tabMinHeight}
/>
</TabsContent>
))}
</Tabs>
) : (
<>
<div className="flex h-10 items-center justify-between border-b border-(--border-subtle,var(--border)) px-4">
{/* oxlint-disable-next-line shadcn/no-arbitrary-values -- reference: header text is 13px */}
<span className="text-[13px] font-medium">{title}</span>
{/* oxlint-disable-next-line shadcn/no-arbitrary-values -- reference: the metric caption is 11px uppercase with wide tracking in the faint grey */}
<span className="text-[11px] font-normal tracking-widest text-(--foreground-faint,var(--muted-foreground)) uppercase">
{metricLabel}
</span>
</div>
<BarList items={items} metric={metric} limit={limit} mono={mono} />
</>
)}
</Reveal>
);
} On this page