busy-office-ui
Menu

Pattern: advanced filter panel

The filter set that outgrew the bar. A finance or purchasing user opens the same list a dozen times a day and narrows it a different way each time — cost centre, status, date range, amount band. They do not want twelve controls permanently across the top; they want the two they use constantly visible and the rest a click away. "Done" means the list reloads and what is currently filtering it stays visible after the panel closes — a filter you cannot see is a filter you will forget you set, and then mistrust the numbers. Not for a list with one obvious narrowing dimension — a single search box or a two-option status filter is already inline in the list report's own bar; reach for the grouped, chip-backed panel here only once distinct facets pile up (cost centre, date band, status, and more), not by default for every list.

The screen

Applied filters, always visible — the answer to "why is this list short?":

Zero new CSS. This is the filter bar for the two controls in constant use, a dropdown menu in its multi-select variant for each group behind a trigger, and chips for what is applied. There is no .bo-filter-panel, and there should not be — the ask was a screen, and the screen is four things that already ship.

The menu is a native [popover], so Escape closes it, clicking away closes it, and focus returns to the trigger — none of that is our code, which is why it behaves the way the rest of the operating system does. Checking a box does not close the menu (you are picking several), and the trigger counts what you picked: "Cost centre (2)".

Why the count on the trigger matters

A collapsed filter group hides its own state. Without the count, "Cost centre" looks identical whether it is filtering nothing or excluding 90% of the rows — and the user reads a short list as missing data rather than as filtered data. That is the single most common support ticket this pattern prevents.

Two channels, as everywhere else: the count is text in the trigger's label, not a coloured dot, so it survives greyscale, forced colours and a screen reader reading the button.

Anatomy

  1. The bar — filter bar; a real <form method="get">, so filters end up in the URL and the screen is shareable and bookmarkable.
  2. Always-visible controls — the one or two people use every time. Search and status here; pick yours from usage, not from symmetry.
  3. Grouped filters — a dropdown menu per group with data-multiselect, real checkboxes, and a name so it submits with the form.
  4. Applied chips — chips, each a link that removes that one filter, plus "Clear all".
  5. The list (not in this demo — this page is scoped to the filter bar itself; see the list report for the paired table) — data table; only it swaps when filters change.

Data contract

RequestReturns / on failure
GET /invoices?status=…&cc=…the list fragment only, swapped into the table region. The bar is not re-rendered — re-rendering it closes an open menu and loses focus mid-selection
GET /invoices (no params)the unfiltered list; the chip row renders empty and "Clear all" is absent, not disabled
Filter optionsrender them server-side into the menu. Fetching options on open costs a round trip on every click and leaves the menu empty at exactly the moment the user is looking at it
422an impossible combination (date-to before date-from): re-render the bar with the values kept and the message beside the offending control — never silently drop a filter

States

StateWhat ships
Nothing filteredno chips, no "Clear all"; triggers show their bare labels
Filtereda chip per applied filter and a count on each trigger — the list is never short without a visible reason
Loadingaria-busy on the table only; the bar stays live so a second filter can be set immediately
Empty because of filtersthe DIFFERENT empty: name which filters are active and offer "Clear all" — not the same message as a genuinely empty list
Empty, unfiltered"No invoices yet" plus the action that creates one
Saved viewchips render from the saved query, so a shared link explains itself on arrival
No permission for a filterthe group is absent, not disabled — a disabled filter tells the user about data they cannot see
Narrow screenthe bar wraps; groups stay behind triggers, which is why this pattern degrades better than twelve inline controls
No JSthe menus are native popover elements opened by popovertarget, so triggers still open and close them and every checkbox inside still submits with the form — but without initDropdowns() the menu is never anchored to its trigger (it falls back to the browser's default popover position) and the trigger label never grows a "(2)" count, so an applied filter is invisible until the menu is reopened

Components used

complexity 2 of 4

↑↓ move Enter open Esc close