Source: https://monarchui.dev/components/filter-bar/

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.

@rookcom/monarch-react/data

Pattern

## Removable filters

## Usage examples

### Search, add filter, editable chip, result count

### Quick filters (toggle chips with counts)

## 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` / `writeFilterParams` convert to and from search params.

## API reference

```tsx
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" calls `onClearAll`.
- Pressing an editable chip opens its `editor` in 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 with `open`).
- `resultCount` changes are announced politely; debounce search with SearchInput’s `debounce`.
- Order: search, active count, chips, add control, "Clear all", then result count and `end` at 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" (`removeLabel` overrides 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.
- `scrollable` keeps one swipeable line; `collapseBelow` moves 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 `writeFilterParams` so 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](https://monarchui.dev/components/data-table/): The table the filters narrow; pass `filtered` for its empty state.
- [SavedViews](https://monarchui.dev/components/saved-views/): Saves a filter combination as a named view.
- [SearchInput](https://monarchui.dev/components/search-input/): The debounced search field for the `search` slot.
- [Toolbar](https://monarchui.dev/components/toolbar/): Place the FilterBar in its `filters` slot.
- Filter Chips (stable): FilterChip registry entry, documented here.
- [SegmentedControl](https://monarchui.dev/components/segmented-control/): Mutually exclusive choices.

## Dependencies

| Kind | Depends on |
| --- | --- |
| npm packages | `react` |
| Monarch components | [Button](https://monarchui.dev/components/button/), [DropdownMenu](https://monarchui.dev/components/dropdown-menu/), [Popover](https://monarchui.dev/components/popover/), [Sheet](https://monarchui.dev/components/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`

## All example code

### Removable filters

```tsx
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 />
```

### Search, add filter, editable chip, result count

```tsx
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)

```tsx
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 />
```
