People need to narrow the data and see which filters are active.
Updated September 10, 2026
Problem
People need to narrow a large dataset and keep track of which choices produced the current view.
Solution
Place shared filtering controls above the content. Show active selections clearly, with a way to remove each one and reset the full set.
Use labels that name the selected values rather than only the filter category. Where possible, update available choices to reflect the current selection. Explain when a combination has no matches.
Preserve filters in shareable links or saved views when appropriate. If selections persist across sessions, keep them visible when people return.
Use when
People need to narrow records using several attributes and may choose different combinations.
Don’t use when
There is one dimension and few values. Three tabs beat a filter bar with one dropdown. Also avoid a filter bar as the only way in: a page whose default state is unfiltered and unusable has pushed its information architecture onto the viewer.
Trade-offs
A growing filter bar can crowd the page. Persistent selections can be forgotten, and incompatible choices can produce confusing empty results. Screenshots need enough visible filter context to be interpreted correctly.
Checklist
Can a viewer see every applied filter without opening a control?
Is removing a filter as cheap as adding one, and is there a clear-all?
Do the offered values reflect the current set, or can a viewer construct an empty result?
If the result is empty, does the page say which filter caused it?
Does the filter state go into the URL?
Do filters persist across sessions, and does the viewer know?
Can filters be chained, so one narrows the options of the next?
How many controls before this stops being a bar and becomes a form?
Does everything on the page honour the filters, and do exceptions say so?
Does a screenshot of this page carry enough to be read correctly?
Compare
Grafana makes filters a declared object rather than an ad-hoc control: template variables are defined on the dashboard, populated by queries, chained so one constrains the next, and carried in the URL, which makes state shareable and every filter something someone had to anticipate. Honeycomb puts a query builder where the bar would be, so narrowing is composing a query and there is no fixed set of filterable attributes—the cost is a higher floor for a casual viewer. Sentry uses a single search field with a tag syntax, trading discoverability for speed once learned, and softens it with autocomplete over real tag values. Datadog scopes a whole page by tags from one control, so filters apply uniformly across every widget by construction rather than by each panel remembering to honour them.
Related
Template variable applies shared values to panel queries. Cross-filter selects data directly in a chart. Search across panels provides text-based navigation.
Saved view preserves useful selections. Ranked list shows an ordering within the selected population.
Wireframe — the pattern's anatomy, not any one product's version of it
Implementation
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.
Deploy 4a91c: canary failing health checksev 1 · 14:30
FilterBar.tsxA chip per applied filter, one click to remove, clear all, a count beside them, and a menu that offers only values the other filters leave.
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<string, string>;/** "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 ( <div className="rounded-lg border bg-card"> <div className="flex flex-wrap items-center gap-2 border-b p-3"> {filters.map((f) => ( <span key={f.key} className="inline-flex items-center gap-1.5 rounded-md border border-chart-1 bg-chart-1/10 py-0.5 pl-2.5 pr-1 text-[11px] text-card-foreground"> {f.key}: {list(f.values)} <Button variant="ghost" size="sm" className="h-5 w-5 p-0 text-chart-1" onClick={() => remove(f.key)} aria-label={`remove ${f.key} filter`}>✕</Button> </span> ))} {filters.length > 0 && ( <Button variant="link" size="sm" className="h-auto p-0 text-[11px] text-muted-foreground underline underline-offset-2" onClick={() => onFiltersChange([])}>clear all</Button> )} <Popover open={open} onOpenChange={setOpen} modal={false}> <PopoverTrigger asChild> <Button variant="outline" size="sm" className="h-6 gap-1 px-2 text-[11px]"><Plus className="size-3" aria-hidden /> filter</Button> </PopoverTrigger> <PopoverContent align="start" className="w-56 p-0"> <Command> <CommandInput placeholder="Narrow by…" className="h-8 text-xs" /> <CommandList> <CommandEmpty>Nothing left to narrow by.</CommandEmpty> {keys.map((key) => ( <CommandGroup key={key} heading={key}> {options(key).map((v) => ( <CommandItem key={v} value={`${key} ${v}`} onSelect={() => add(key, v)} className="text-xs">{v}</CommandItem> ))} </CommandGroup> ))} </CommandList> </Command> </PopoverContent> </Popover> <span className="ml-auto text-[11px] tabular-nums text-muted-foreground" aria-live="polite"> {matches.length.toLocaleString("en-US")} of {rows.length.toLocaleString("en-US")} {noun} </span> </div> {culprit ? ( <p className="p-3 text-xs text-muted-foreground"> Nothing matches. Removing <span className="text-card-foreground">{culprit.key}: {list(culprit.values)}</span> would bring {noun} back. </p> ) : children(matches)} </div> );}
demo.tsxHow it is called: the rows, the filterable keys, the applied filters in state, and a render function for what passes.
Screenshots are grouped by product and include the capture date.
Dashed boxes highlight this pattern. Notes appear below each screenshot and when you hover over a box.
Emergency Department / Clinical Dashboard September 11, 2026Tableau Public embed view; emergency department patient flow workbookdense · light · desktop-web
This workbook analyzes emergency-department activity rather than showing a live bed board. Its hour-by-weekday grid makes recurring busy periods easy to locate. Monthly small multiples support seasonal comparison on a shared scale. The color ramps need careful labeling: a diverging scale on a count or duration needs a meaningful center, not an unexplained midpoint.
Examples / Alert List September 10, 2026Grafana Play (signed out; no version string exposed)medium · dark · desktop-web
This list shows seven alerts with their states and durations. Several have been firing for days, which raises a question about whether the rules still prompt a useful response. The names use inconsistent conventions and often do not describe the affected system clearly. Pending and firing states are labeled separately, and each row links back to its rule.
Alert rule attached to panelAlert names use different conventions and do not consistently identify the affected system.
Demo / Annotations September 10, 2026Grafana Play (signed out; no version string exposed)medium · dark · desktop-web
Frequent red event markers make the request-rate line difficult to follow across this twenty-four-hour view. The tag filter is already active, so filtering alone has not reduced the crowding enough. The event list below is easier to read because it separates timestamps and tags into individual entries. This example shows why event density matters as much as recording the events.
AnnotationFrequent event markers compete with the request-rate line.
AnnotationThe event list provides readable timestamps and tags.
Filter barA tag filter is already applied to the annotations.
Time-range pickerThe selected twenty-four-hour range affects how densely markers appear.
Examples / Dashboard Variables September 10, 2026Grafana Play (signed out; no version string exposed)medium · dark · desktop-web
This dashboard demonstrates six selectors and explains how their values affect the page. Selected values appear as removable chips, making the active scope easy to read. A text panel also describes a hidden variable that has no visible control. Another note explains that changing the data source clears an instance selection. These dependencies need clear feedback when people change a value.
Template variableSelectors provide values used by panel queries and shared in the URL.
Filter barSelected values appear as removable chips.
Explain this metricA text panel explains a hidden variable and its current value.
Discover / filebeat logs September 10, 2026Elastic demo environment (guest session; no version string exposed)dense · light · desktop-web
The view places a searchable field list beside a large table of documents, with a volume chart above. Search is useful for navigating the hundreds of available fields. The default Summary column combines many fields into each row, making quick comparisons difficult until people choose more focused columns. Pagination and sorting are visible, but the scope of each operation needs to be clear.
Dashboards / [Flights] Global Flight Dashboard September 10, 2026Elastic demo environment, sample flight data (guest session)dense · light · desktop-web
The delay table shows percentages without the number of flights behind each one. A high rate based on very few flights would look as prominent as one based on many. A nearby comparison tile names the previous week but does not clearly explain what its percentage represents. The filtering controls are more explicit: a query bar sits above pickers and a price range, giving people both typed and guided ways to narrow the view.
Ratio and rateDelay percentages appear without their flight counts.
Compare periodsThe comparison period is named, but the meaning of the percentage is unclear.
Share and embedSharing and full-screen controls are available in the header.
Filter barPickers and a range control provide alternatives to typing a query.
Panel gridPanels use different sizes within a grid.
Stacked compositionA 100% stack shows the mix of delay types without total volume.
AnnotationGrouped event markers share the chart’s time axis.
Header KPI stripDifferent tile sizes and arrangements make the summary row less consistent.