For the complete documentation index, see llms.txt. Markdown variants are available by appending .md to documentation URLs. Homepage markdown is available at /llms.md/content.md. An agent skill is available at /.well-known/agent-skills/site-skill.md.

Bar List Card 01

A compact breakdown with proportional bars and optional tabs.

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>
  );
}