Filter bar Narrowing a large set by several attributes, with what is applied visible without opening anything. Applied filters belong as removable chips, and the result count belongs beside them so an empty result has an explanation. npx shadcn@latest add button popover command npm i lucide-react Tokens this needs: --card, --card-foreground, --muted-foreground, --border, --chart-1 ──────────────────────────────────────────────────────────────────────── // FilterBar.tsx ──────────────────────────────────────────────────────────────────────── import { useState, type ReactNode } from "react"; import { Plus } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; /** One narrowing: a dimension and the values allowed through. */ export type Filter = { key: string; values: string[] }; export type Row = Record; /** "1 and 2", "checkout", "a, b and c". A chip says the values, never a count. */ const list = (v: string[]) => (v.length < 3 ? v.join(" and ") : `${v.slice(0, -1).join(", ")} and ${v[v.length - 1]}`); const passes = (row: Row, filters: Filter[]) => filters.every((f) => f.values.includes(row[f.key])); /** * The state is the bar; the content is a consequence. Every applied filter is * its own chip, removal is one click, and the offered values come from the * rows the other filters leave, so the viewer cannot build an empty result * from the menu. The count sits beside the chips, and an empty one says which * chip did it. */ export function FilterBar({ rows, keys, filters, onFiltersChange, noun = "rows", children }: { rows: Row[]; /** The dimensions a viewer may filter on, in menu order. */ keys: string[]; filters: Filter[]; onFiltersChange: (filters: Filter[]) => void; noun?: string; /** Renders the rows that pass. */ children: (rows: Row[]) => ReactNode; }) { const [open, setOpen] = useState(false); const matches = rows.filter((r) => passes(r, filters)); // Which chip emptied the page: the one whose removal brings rows back. const culprit = matches.length === 0 ? filters.find((f) => rows.some((r) => passes(r, filters.filter((g) => g !== f)))) : undefined; const remove = (key: string) => onFiltersChange(filters.filter((f) => f.key !== key)); const add = (key: string, value: string) => { const cur = filters.find((f) => f.key === key); onFiltersChange(cur ? filters.map((f) => (f.key === key ? { key, values: [...f.values, value] } : f)) : [...filters, { key, values: [value] }]); setOpen(false); }; // Options for a key come from rows that pass every other filter. const options = (key: string) => { const others = filters.filter((f) => f.key !== key); const chosen = filters.find((f) => f.key === key)?.values ?? []; return [...new Set(rows.filter((r) => passes(r, others)).map((r) => r[key]))].filter((v) => !chosen.includes(v)).sort(); }; return (
{filters.map((f) => ( {f.key}: {list(f.values)} ))} {filters.length > 0 && ( )} Nothing left to narrow by. {keys.map((key) => ( {options(key).map((v) => ( add(key, v)} className="text-xs">{v} ))} ))} {matches.length.toLocaleString("en-US")} of {rows.length.toLocaleString("en-US")} {noun}
{culprit ? (

Nothing matches. Removing {culprit.key}: {list(culprit.values)} would bring {noun} back.

) : children(matches)}
); } ──────────────────────────────────────────────────────────────────────── // demo.tsx ──────────────────────────────────────────────────────────────────────── import { useState } from "react"; import { FilterBar, type Filter, type Row } from "./FilterBar"; /** Alerts from the last hour. Three filters applied, four rows left. */ const ALERTS: Row[] = [ { at: "14:52", service: "checkout", env: "prod", sev: "1", title: "p95 latency above 800 ms for 5 min" }, { at: "14:47", service: "checkout", env: "prod", sev: "2", title: "Card auth error rate 3.1% (threshold 2%)" }, { at: "14:41", service: "checkout", env: "prod", sev: "2", title: "Queue depth 4,200 and rising" }, { at: "14:30", service: "checkout", env: "prod", sev: "1", title: "Deploy 4a91c: canary failing health check" }, { at: "14:28", service: "checkout", env: "prod", sev: "3", title: "Cache hit ratio below 90%" }, { at: "14:22", service: "checkout", env: "staging", sev: "1", title: "Pod restarting in a loop" }, { at: "14:15", service: "search", env: "prod", sev: "2", title: "Index lag 90 s" }, { at: "14:09", service: "cart", env: "prod", sev: "1", title: "Redis primary failover" }, { at: "13:58", service: "search", env: "staging", sev: "3", title: "Slow query logged 12 times" }, ]; export default function Demo() { const [filters, setFilters] = useState([ { key: "service", values: ["checkout"] }, { key: "env", values: ["prod"] }, { key: "sev", values: ["1", "2"] }, ]); return ( {(rows) => (
    {rows.map((r) => (
  • {r.title} sev {r.sev} · {r.at}
  • ))}
)}
); }