Filter bar
Narrows a long list down to the rows someone came for.
Examples
Conversations240
240 rows, sorted by Conversation descending
Conversations40
40 rows, sorted by Conversation descending
Choice
singledefault
multiple
Add filter
emptydefault
set
Sizes
xs
defaultdefault
States
rest
hover
focus-visible
active
disabled
Usage
When to use it
- Use
- Above a list or table of 20 or more rows that people narrow by text and by known fields.
- Use
- The person needs to see how many rows are left at every step.
- Use
- Filters combine: a status and an agent and a model at once.
- Not for
- Switching between two to five fixed views of the same data. Use Segmented control.
- Not for
- Searching across the whole app, not one list. Use Command.
- Not for
- Picking a value that a form submits. Use Select.
Rules
12 rules
- Must
- Pass each option count from useFilteredRows, counted against the search, the other filters and pressed toggles. Because a count against the whole list promises rows the other filters already hide, so the pick empties the table.
- Must
- End the bar with FilterBarCount. Because its status region is the only thing a screen reader hears when the list changes.
- Must
- Give FilterBar an aria-label naming what it filters. Because FilterBar is a role="search" landmark, and unnamed ones read alike in the landmark list.
- Must
- Render FilterBarClear only while a filter or the search is set. Because a Clear all with nothing to clear is a dead tab stop that suggests a filter is still on.
- Must
- Keep shortcut="/" on the main bar and pass shortcut={null} to every other bar on the page. Because only the first bar on the page takes "/", so a second bar shows a hint for a key that focuses another field.
- Should
- Name a select with the column's one-word name; the chip adds the value.
- Should
- Name the noun in the search placeholder.
- Never
- Add an Apply button; apply each change as it happens. Because until the press the chips say one thing and the list another, and the count announces nothing.
- Never
- Hide filters behind a funnel icon button; put the two to four most used on the bar. Because a funnel hides which filters are set and adds a click before every pick.
- Should
- Use FilterBarAdd for a log or feed whose filters are mostly off, one value per field; use FilterBarSelect for a field that takes several values or shows counts. Because the dashed chip lists what can narrow the list without spending a row on it, but it can hold only one value.
- Should
- Keep 4 selects on the bar and move the rest behind a More filters menu.
- Should
- Pass the bar to DataTable's toolbar so it shares the table's frame.
Accessibility
6 checks
- The bar is a search landmark; the search is a searchbox with aria-keyshortcuts="/".
- A select chip is a button with aria-haspopup and aria-expanded; options are menuitemcheckbox or menuitemradio with aria-checked.
- / focuses the search outside fields, menus and dialogs, and not during IME composition. Escape clears the text.
- Removing a chip moves focus to the next chip; after the last, to the select that set it.
- Clear all moves focus to the search.
- FilterBarCount is a polite status; the announced copy waits 500ms without change.
Edge cases
11 edge cases
- Long select value
- Truncates inside the chip; the full value is the accessible name.
- Right to left
- Search icon, clear and hint sit on the logical sides.
- Text spacing
- Chips widen, values truncate, the bar wraps.
- Filters leave 0 rows
- Count reads "0 of 240 conversations"; the list says why. Clear all in the bar is the one reset, so the empty row adds no Clear button.
- Select with no options
- Pass disabled instead of opening an empty menu.
- Option count 0 and chosen
- Stays in place so it can be unchecked.
- Server-side search
- The bar stays live; counts show the last result until the next arrives.
- Failed fetch
- Show Empty state reason="error" in the list; the bar keeps its values.
- Typing / in a dialog, menu or open listbox
- The shortcut does nothing there.
- Forced colors
- Chips take a 1px outline, pressed 2px.
- Inside a DataTable toolbar
- The strip is --row (40px) and spans the frame; cells drop their edges for the column rule.
API
18 props
FilterBardiv propsFilterBarChips rowbooleandefaultfalseFilterBarSearch value, onValueChangestring, (value) => voiddefaultrequiredFilterBarSearch shortcutstring | nulldefault'/'FilterBarSelect label, optionsstring, FilterOption[]defaultrequiredFilterBarSelect multiplebooleandefaultfalseFilterBarSelect showValuebooleandefaulttrueFilterBarSelect showAllOption, allLabelboolean, stringdefaultfalse, 'All'FilterBarSelect sinkEmptybooleandefaulttrue with countsFilterBarSelect size'xs' | 'sm' | 'default'default'default'FilterBarAdd label, optionsstring, FilterOption[]defaultrequiredFilterBarAdd value, onValueChangestring | null, (value) => voiddefaultnullFilterChip label, value, kind, onRemovestring, ReactNode, Kind, () => voiddefaultrequiredFilterTogglepressed, onPressedChangeFilterBarCount shown, total, nounnumber, number, stringdefaultrequireduseFilteredRows(rows, {search, match, fields, filters, where})fields, filters{id: string; value: (row) => string | string[]}[], Record<string, string[]>match, where(row, query) => boolean, ((row) => boolean | false)[]
Tokens
20 tokens
--surface-page- var(--paper)
--line-control- oklch(var(--ink-c) / 0.16)
--line-control-hover- oklch(var(--ink-c) / 0.32)
--line-row- oklch(var(--ink-c) / 0.08)
--surface-selected- oklch(var(--ink-c) / 0.09)
--control-sm- 28px
--row- 40px
--type-label- 400 14px/16px var(--font-sans)
--text-3- oklch(var(--ink-c) / 0.66)
--radius-soft- 6px
--radius-inner- 4px
--radius-frame- 10px
--elev-overlay- 0 0 0 1px var(--line-frame), 0 12px 24px -12px oklch(var(--text-on-field-c) / var(--shadow-alpha))
--dur-base- 160ms
--dur-fast- 120ms
--ease-step- cubic-bezier(0.2, 0, 0, 1)
--surface-hover- oklch(var(--ink-c) / 0.05)
--type-label-sm- 400 12px/16px var(--font-sans)
--type-body-sm- 380 13px/18px var(--font-sans)
--text-1- var(--ink)