registry
@automindzsheets 01–77shadcn 4.x
SHEET 14

Filter Bar

block / data / filter

Faceted filters whose state lives in the URL: single select, multi-select, debounced text and a native date range, with removable chips and a shareable link.

type
block
files
2
registry deps
badge, button, input, label, popover, @automindz/combobox, @automindz/empty-state, @automindz/multi-select
npm deps
lucide-react

installs

  • lib/automindz/facets.ts
  • components/automindz/filter-bar.tsx
  • Facets are data — id, label, kind, options or loadOptions
  • The URL is the state: shareable, reload-proof, replace-only writes
  • Controls are @automindz/combobox and @automindz/multi-select, not copies
  • One chip per active value, and the chip is the button
  • Narrow: every facet folds into one Filters popover

Facets are data: { id, label, kind } — select, multi, text or date-range, with options or loadOptions. **id is the URL key.** The bar owns the URL: read it with parseFilters(facets, searchParams) and pass the values to your table as rows, so the two never fight. Multi values repeat their key, empty ones are dropped, writes are replace. Calls useSearchParams — wrap it in <Suspense>. Its lib is facets.ts, not filter-bar.ts: shadcn add rewrites imports by basename.

Variations

one state per section

01

Filters in the address bar

Pick a status, then look at the address bar: ?status=active. Nothing is mirrored into component state — the bar derives from useSearchParams() and writes back with router.replace, so there is no second copy of the truth to drift. Copy the URL into a new tab and the same filter set comes back, which is the whole point: a filtered view is a link you can send someone. The dashed line under the bar prints the query string this bar owns.

fluid
02

Driving a real data table

The pairing this item exists for. The bar owns the URL; the table receives the resolved rows as a prop — parseFilters → your own matching → rows — and its own searchable is deliberately off, because two search boxes over the same rows is exactly how the two end up fighting. Sorting, pagination and selection stay the table's business: they are view state, not a shareable filter. Narrow the set while on page 2 and the pager clamps rather than showing an empty body.

fluid
03

Four control types, one contract

One Facet shape covers all four kinds. select renders @automindz/combobox and multi renders @automindz/multi-select — neither is reimplemented here, which is the single-source rule and also dogfoods the registry. text debounces. date-range is two native <input type="date">: the platform already ships a keyboard-operable, localised date control, and a range filter is not worth a picker library. Owner fetches its options through loadOptions; the others are inline arrays.

fluid
04

Active filters, visible and reversible

One chip per active value — not per facet: two plans are two chips, and a date range is its two bounds, each removable on its own. The chip *is* the button — nothing interactive nests inside it, which is the invalid, keyboard-unreachable markup multi-select shipped with first. Load the example, then remove chips one at a time: the count follows, and the last removal returns to the bare path rather than leaving a dangling ?.

fluid
05

Mid-debounce

The text facet at debounceMs: 900, so the gap is visible. A keystroke lands in local state, the input goes aria-busy with a spinner, and only the settled value reaches the URL — that is what stops the address bar taking a write per character. Enter or blur flushes it early. Every write is a replace, so history.length does not move however long you type.

fluid
06

Options still loading

Owner's loadOptions has not resolved yet: the control keeps its slot at its real size, shows a spinner in place of the caret and says so when opened, so nothing jumps when the list lands. The other facets stay usable — one slow facet does not hold up the bar.

fluid
07

Options failed to load

A rejected loadOptions renders in place, with the reason and a retry, rather than an empty dropdown that looks like “no owners exist”. The facet keeps its slot and goes blocked instead of vanishing — a filter that disappears makes people ask where it went. Announced through role="alert", and the failure is carried by text and an icon, never by the colour alone. Press “Try again”: this one succeeds.

fluid
08

Blocked facet, with the reason

Different from loading and different from failed: disabled plus disabledReason. The control is really disabled — no hover, no press — and the reason sits under it in the reading order, because a greyed-out control with no explanation is a support ticket.

fluid
09

Nothing matched

A filter set with no rows behind it. The table's body renders @automindz/empty-state's search variant through FilterNoResults, which ships with this item so every consumer says the same thing — and its action actually clears the filters instead of telling you to. Distinct from “nothing created yet”, because the fix is different.

fluid
10

Narrow container

Held at 320px. Below the collapse width every facet folds into one “Filters” popover carrying the active count, and the chips stay outside it. A popover rather than a drawer: a drawer is a modal, and covering the rows while you filter them hides the only feedback the controls have — and it needs no dependency the controls did not already bring. It collapses on the bar's own container width, so a bar in a narrow column collapses on a wide screen too.

fluid