Collapsible row Sections that fold, with the header carrying enough state that folding costs nothing. A collapsed row hiding a firing alert is the failure case, so the count belongs on the header. npx shadcn@latest add collapsible card npm i lucide-react Tokens this needs: --card, --card-foreground, --muted-foreground, --border, --status-critical The status, chart, scale, state and direction names are an extension, not a rename. shadcn has --destructive and five --chart-* and nothing else in this territory. ──────────────────────────────────────────────────────────────────────── // CollapsibleRows.tsx ──────────────────────────────────────────────────────────────────────── import { useState, type ReactNode } from "react"; import { ChevronRight } from "lucide-react"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { cn } from "@/lib/utils"; export type Section = { id: string; /** The whole design. "Advanced" hides its contents from everyone forever; * "Disk and filesystem" is a promise specific enough to spend a click on. */ title: string; panels: ReactNode[]; /** Alerts firing inside, by name. A folded row still says so, because a * page that looks healthy with the unhealthy part folded away is worse * than a long page. */ firing?: string[]; }; type Props = { sections: Section[]; /** Which rows start open. The open set should answer "is it okay"; the * folded rows answer why. */ defaultOpen: string[]; /** Remember it per viewer, and carry it in a shared link. */ onToggle?: (id: string, open: boolean) => void; }; export function CollapsibleRows({ sections, defaultOpen, onToggle }: Props) { const [open, setOpen] = useState(() => new Set(defaultOpen)); const toggle = (id: string, next: boolean) => { const s = new Set(open); next ? s.add(id) : s.delete(id); setOpen(s); onToggle?.(id, next); }; return (
{sections.map((s) => { const isOpen = open.has(s.id); const firing = s.firing ?? []; return ( toggle(s.id, o)} className="rounded-lg border bg-card"> {s.title} {firing.length > 0 && ( )} {/* The count is what is behind the click, so it shows only while the row is folded. */} {!isOpen && ( {s.panels.length} panels )}
{s.panels}
); })}
); } ──────────────────────────────────────────────────────────────────────── // demo.tsx ──────────────────────────────────────────────────────────────────────── import { Card } from "@/components/ui/card"; import { KpiTile } from "../kpi-tile/KpiTile"; import { CollapsibleRows, type Section } from "./CollapsibleRows"; /** * Three sections. Service health is open and answers "is it okay". Disk is * folded with its count. JVM is folded too, and two alerts inside it show on * the header so folding it cost nothing. */ const NOW = new Date("2026-09-15T09:00:00Z"); const ago = (s: number) => new Date(NOW.getTime() - s * 1000); const Stat = ({ label, value }: { label: string; value: string }) => (

{label}

{value}

); const DISK: [string, string][] = [ ["Root used", "61%"], ["/var used", "78%"], ["/data used", "43%"], ["Inodes root", "12%"], ["Read IOPS", "1.2k"], ["Write IOPS", "640"], ["Read latency", "3.1 ms"], ["Write latency", "5.4 ms"], ["Queue depth", "2"], ["Throughput", "210 MB/s"], ["I/O errors", "0"], ["Days until full", "41"], ]; const JVM: [string, string][] = [ ["Heap used", "92%"], ["GC pause p99", "1.8 s"], ["Old gen", "88%"], ["Threads", "412"], ["Young GC / min", "14"], ["Full GC / hr", "3"], ]; const SECTIONS: Section[] = [ { id: "health", title: "Service health", panels: [ , , , ], }, { id: "disk", title: "Disk and filesystem", panels: DISK.map(([l, v]) => ) }, { id: "jvm", title: "JVM and garbage collection", firing: ["Heap used above 90%", "GC pause p99 above 1 s"], panels: JVM.map(([l, v]) => ), }, ]; export default function Demo() { return (
); }