Patterns · Search and discovery
FilterBar
Filter row: search, removable, editable or toggle FilterChips, an "Add filter" menu, "Clear all", a result count and trailing controls.
Removable filters
import { useState } from 'react';
import { FilterBar, FilterChip } from '@rookcom/monarch-react';
const INITIAL = [
{ name: 'Status', value: 'Open' },
{ name: 'Assignee', value: 'Me' },
{ name: 'Priority', value: 'High' },
];
function ActiveFilters() {
const [filters, setFilters] = useState(INITIAL);
return (
<FilterBar {...(filters.length ? { onClearAll: () => setFilters([]) } : {})}>
{filters.map((f) => (
<FilterChip
key={f.name}
name={f.name}
value={f.value}
onRemove={() => setFilters((all) => all.filter((x) => x.name !== f.name))}
/>
))}
</FilterBar>
);
}
<ActiveFilters />Usage examples
Search, add filter, editable chip, result count
import { useState } from 'react';
import { Button, FilterBar, FilterChip, SearchInput } from '@rookcom/monarch-react';
import { FilterMenu } from '@rookcom/monarch-react/data';
const STATUSES = ['Open', 'Pending', 'Closed'];
function FullBar() {
const [query, setQuery] = useState('');
const [status, setStatus] = useState<string | null>('Open');
const [owner, setOwner] = useState<string | null>(null);
const [statusOpen, setStatusOpen] = useState(false);
const active = [status, owner].filter(Boolean).length;
return (
<FilterBar
landmark
label="Filter tickets"
search={
<SearchInput
aria-label="Search tickets"
placeholder="Search tickets"
size="sm"
value={query}
onValueChange={setQuery}
debounce={250}
/>
}
activeCount={active}
resultCount={`${active ? 18 : 64} results`}
end={
<Button size="sm" variant="ghost">
Newest first
</Button>
}
add={
<FilterMenu
options={[
{ value: 'status', label: 'Status', disabled: status !== null },
{ value: 'owner', label: 'Owner', disabled: owner !== null },
]}
onSelect={(v) => {
if (v === 'status') {
setStatus('Open');
setStatusOpen(true);
} else setOwner('Me');
}}
/>
}
{...(active
? {
onClearAll: () => {
setStatus(null);
setOwner(null);
},
}
: {})}
>
{status ? (
<FilterChip
name="Status"
value={status}
open={statusOpen}
onOpenChange={setStatusOpen}
onRemove={() => setStatus(null)}
editor={
<div style={{ display: 'grid', gap: 4 }}>
{STATUSES.map((s) => (
<Button
key={s}
size="xs"
variant={s === status ? 'primary' : 'ghost'}
onClick={() => {
setStatus(s);
setStatusOpen(false);
}}
>
{s}
</Button>
))}
</div>
}
/>
) : null}
{owner ? <FilterChip name="Owner" value={owner} onRemove={() => setOwner(null)} /> : null}
</FilterBar>
);
}
<FullBar />Quick filters (toggle chips with counts)
import { useState } from 'react';
import { FilterBar, FilterChip } from '@rookcom/monarch-react';
function QuickFilters() {
const [on, setOn] = useState<Record<string, boolean>>({ mine: true });
const chips = [
{ id: 'mine', name: 'Assigned to me', count: 12 },
{ id: 'unread', name: 'Unread', count: 5 },
{ id: 'vip', name: 'VIP customers', count: 3 },
{ id: 'sla', name: 'SLA at risk', count: 2 },
];
return (
<FilterBar label="Quick filters" scrollable>
{chips.map((c) => (
<FilterChip
key={c.id}
name={c.name}
count={c.count}
pressed={!!on[c.id]}
onPressedChange={(p) => setOn((all) => ({ ...all, [c.id]: p }))}
/>
))}
</FilterBar>
);
}
<QuickFilters />Purpose
Shows which filters are applied to a list and lets people change them: search, add a filter, edit or remove one, toggle quick filters, or clear everything, with the result count beside it. FilterChip is one filter; FilterMenu is the "Add filter" menu.
Use when
- A list or table has filters applied and the user needs to see, change and remove them.
- Quick one-tap filters ("Assigned to me", "Unread") with counts: FilterChip in toggle mode.
Avoid when
- Switching between mutually exclusive views: use Tabs or SegmentedControl.
- Tags on a record: use Badge.
Design principles
- Chips show field and value ("Status: Open") so filters read without context.
- The app owns filter state and the filter definitions; FilterBar renders it and reports changes. Pickers (selects, date ranges, people) go in a chip’s
editor. - "Clear all" appears only when the app passes
onClearAll. - Toggle chips use
aria-pressed, never colour alone. - URL state is router-agnostic:
readFilterParams/writeFilterParamsconvert to and from search params.
API reference
import { FilterBar, FilterMenu, defaultFilterBarMessages, readFilterParams, writeFilterParams, FilterChip } from '@rookcom/monarch-react/data';| Export | Kind | Rendering | Root class |
|---|---|---|---|
FilterBar | component | client | .mn-filter-chip |
FilterMenu | component | client | .mn-filter-chip |
defaultFilterBarMessages | utility | client | – |
readFilterParams | utility | client | – |
writeFilterParams | utility | client | – |
FilterChip | component | client | .mn-filter-chip |
FilterBar and FilterChip are exported from @rookcom/monarch-react (unchanged) and @rookcom/monarch-react/data; FilterMenu, readFilterParams and writeFilterParams from @rookcom/monarch-react/data.
FilterChip has three modes: removable/read-only (default), editable (editor opens a popover from the name and value), and toggle (pressed, defaultPressed or onPressedChange make the whole chip a pressed button).
They render plain elements and pass native attributes through; no refs are forwarded.
messages translates "2 filters", "Filters (n)", the sheet title and "Done".
Generated from the TypeScript declarations of packages/react/src/patterns/search/filter-bar.
Properties
FilterBar
Filter row: search, FilterChips, an add control, "Clear all", a result count and trailing controls.
| Prop | Type | Description |
|---|---|---|
activeCount | number | Number of applied filters. Shows "2 filters" and is used by the narrow-screen button. |
add | ReactNode | Slot for an "Add filter" control (FilterMenu, or your own). |
children | ReactNode | |
className | string | |
clearAllLabel | string | Hide "Clear all" when there is nothing to clear. |
collapseBelow | number | Below this width (px) of the bar, chips, the add control and "Clear all" move into a bottom sheet opened by a "Filters (n)" button. |
end | ReactNode | Controls on the trailing edge: sort, display options, saved views. |
id | string | |
label | string | Accessible name of the group. |
landmark | boolean | Expose the bar as a search landmark (`role="search"`) instead of a group. |
messages | Partial<FilterBarMessages> | |
onClearAll | (() => void) | Called by the "Clear all" button; the button is hidden when omitted. |
resultCount | ReactNode | Result summary, e.g. "24 results". Announced politely when it changes. |
scrollable | boolean | Keep chips on one line that scrolls sideways; the pressed chip scrolls into view. |
search | ReactNode | Search field on the leading edge, e.g. a SearchInput with `debounce`. |
FilterMenu
"Add filter" menu listing the filter fields; put it in FilterBar's `add` slot.
| Prop | Type | Description |
|---|---|---|
onSelect required | (value: string) => void | A field was chosen; open its editor (for example a FilterChip with `editor` and `open`). |
options required | readonly FilterMenuOption[] | Filter fields the user can add. |
className | string | |
disabled | boolean | |
label | string | Trigger text. Default "Add filter". |
size | "xs" | "sm" | "md" |
FilterChip
One active filter: removable, editable in a popover, or a pressed toggle.
| Prop | Type | Description |
|---|---|---|
name required | string | Filter field, e.g. "Status". In toggle mode, the chip's label, e.g. "Unread". |
className | string | |
contentLabel | string | Accessible name of the popover. Default "<name> filter". |
count | number | Count shown after the label, e.g. matching items. |
defaultPressed | boolean | Initial pressed state (uncontrolled toggle mode). |
editor | ReactNode | Editor shown in a popover when the chip is pressed (pickers for the value). The name and value become the popover trigger. |
id | string | |
onOpenChange | ((open: boolean) => void) | |
onPressedChange | ((pressed: boolean) => void) | |
onRemove | (() => void) | Removes the filter. The remove button is labelled "Remove <name> filter". |
open | boolean | Popover open state (controlled). |
pressed | boolean | Toggle mode: the whole chip is a pressed/unpressed button (`aria-pressed`), for quick filters such as "Assigned to me". Controlled. |
removeLabel | string | Accessible name of the remove button. Default "Remove <name> filter". |
value | ReactNode | Current value, e.g. "Open". Optional for toggle chips. |
Variants
| Prop | Values | Description |
|---|---|---|
FilterChip mode | 'read-only''removable (onRemove)''editable (editor)''toggle (pressed)' | Toggle chips are data-variant="toggle" with data-pressed; editable chips are data-editable. |
count (FilterChip) | 'number' | A count after the label, e.g. matching items. |
landmark | 'true''false' | role="search" instead of role="group". |
scrollable | 'true''false' | Chips stay on one line that scrolls sideways; the pressed chip scrolls into view. |
collapseBelow | 'number (px)' | Below that width of the bar, chips, the add control and "Clear all" move into a bottom Sheet opened by "Filters (n)". |
States
| State | Exposed as | Behaviour |
|---|---|---|
| Pressed | aria-pressed, data-pressed | Toggle chip is on: accent border and fill. |
| Editing | aria-expanded | The chip’s popover is open (controlled with open/onOpenChange). |
| Collapsed | data-collapsed | Narrow bar: filters are in the sheet. |
| Hover and focus | – | Chip buttons and "Clear all" have hover backgrounds and a visible focus ring. |
Interaction behaviour
- The remove button calls
onRemove; "Clear all" callsonClearAll. - Pressing an editable chip opens its
editorin a popover; Escape closes it and returns focus to the chip. - Pressing a toggle chip flips it and calls
onPressedChange. - FilterMenu lists the fields; choosing one calls
onSelect(open the new chip’s editor withopen). resultCountchanges are announced politely; debounce search with SearchInput’sdebounce.- Order: search, active count, chips, add control, "Clear all", then result count and
endat the trailing edge.
Keyboard support
| Keys | Action |
|---|---|
| Tab | Moves through search, chip buttons, remove buttons, add and "Clear all". |
| Enter / Space | Removes, toggles, opens a chip editor, or clears all. |
| Escape | Closes a chip editor, the add menu or the filter sheet. |
| Arrow keys | Move within the FilterMenu. |
Accessibility
- Role
- group, or search with
landmark - Accessible name
label, default "Active filters".
- Each remove button is labelled "Remove <name> filter" (
removeLabeloverrides it). - Toggle chips are buttons with
aria-pressed; their count is part of the name. - Chip editors are labelled popovers ("Status filter").
- The result count is a status region; the narrow-screen button states how many filters are active.
- Icons are decorative.
Responsive behaviour
- Chips wrap onto new lines by default; the search field shrinks to fit.
scrollablekeeps one swipeable line;collapseBelowmoves filters into a bottom sheet on phones.
Theme support
- Chip, pressed, count and focus colours use Monarch semantic tokens, so every theme and contrast mode applies.
Do and don't
Do
- Move focus somewhere sensible after a removal (the list or the next chip).
- Hide "Clear all" when nothing is applied.
- Keep filters in the URL with
writeFilterParamsso views can be shared and reloaded.
Don't
- Do not use chips for values the user cannot remove without explaining why.
- Do not use toggle chips for mutually exclusive options; use SegmentedControl or Tabs.
Related components
- DataTable: The table the filters narrow; pass
filteredfor its empty state. - SavedViews: Saves a filter combination as a named view.
- SearchInput: The debounced search field for the
searchslot. - Toolbar: Place the FilterBar in its
filtersslot. - Filter Chips (stable): FilterChip registry entry, documented here.
- SegmentedControl: Mutually exclusive choices.
Dependencies
| Kind | Depends on |
|---|---|
| npm packages | react |
| Monarch components | Button, DropdownMenu, Popover, Sheet |
Testing coverage
- Remove buttons are labelled per filter and call onRemove; "Clear all" calls onClearAll; default group name.
- Toggle chips expose aria-pressed and counts, controlled and uncontrolled.
- Editable chip opens a labelled popover and Escape returns focus.
- Search, active count, result status region, trailing slot and landmark role.
- collapseBelow moves filters into a sheet with Clear all and Done; focus returns to the trigger.
- FilterMenu adds a field by keyboard; readFilterParams / writeFilterParams round-trip.
| Check | Covered |
|---|---|
| Axe in jsdom | Yes |
| Server rendering | Yes |
| Browser suites | None yet |
Tests: packages/react/src/patterns/search/filter-bar