Filters
A filter bar of segmented pills with per-field operators and value controls
The filter bar shows active filters as segmented pills, each pairing a field, an operator, and a value, plus an "Add filter" button that opens a searchable field picker. Use it for list and table views where users refine results by several conditions at once, the pattern you see in Linear, Notion, and Stripe.
For a single dropdown of options tied to a form field, use Combobox instead.
Preview
Installation
Usage
Composition
Drive the bar with a fields config and a value array of active filters. Each field's type picks the value control and its default operators. Without children, the bar renders FilterChips, FilterAddButton, and a FilterClearButton once filters exist; pass children to compose the same parts in any order.
Features
- Per-type operators with sensible defaults (
is,contains,between,is empty), overridable or hideable per field. - Operators declare a value
shape("none"/"scalar"/"range"), so custom operators get the right control automatically;is emptydrops the value segment, and any"range"operator gets paired min/max inputs. - Adding a filter opens its value picker (or focuses its input) right away, so you pick in one gesture.
- Colored dots or icons on options keep a dense bar scannable; fields accept an optional
icontoo. prefix/suffixon text and number fields for currency, units, or symbols;maxSelectionscaps a multiselect.- Number fields are spinner-free (Base UI) with arrow-key stepping and wheel scrubbing (scroll to adjust while focused).
- An optional
shortcutkey opens the add-filter menu, with aKbdhint on the button. - Controlled and uncontrolled: pass
value+onValueChange, ordefaultValue. Because it is controlled, syncing to the URL (nuqs) is trivial. - The
customfield type renders any control throughrenderValue, for dates, sliders, or bespoke inputs. - Searchable, keyboard-navigable field and value pickers, inherited from
Combobox.
Examples
Controlled
Pass value and onValueChange to own the state; the serialized filters update as you edit. This example also composes a FilterActiveCount badge into the bar.
[]
Field types
Use type to pick the value control. select and multiselect open a searchable list, text and number render inline inputs, and custom renders your own control through renderValue.
| Type | Value control | Value shape |
|---|---|---|
"select" | Searchable single-select list. | string | null |
"multiselect" | Searchable multi-select list. | string[] |
"text" | Inline text input. | string |
"number" | Inline number input, paired for between. | number | null | NumberRange |
"custom" | Your control via renderValue. | unknown |
Prefix and suffix
Add a prefix or suffix to text and number fields for currency, units, or symbols. The value input stays inline; the affix sits alongside it.
Operators
Use operators to replace the default set for a field, or disabledOperators to remove specific ones. Each operator declares a shape: "none" hides the value segment (valueless: true is sugar for it), "range" renders paired min/max inputs on number fields, and "scalar" (the default) renders the field's normal control.
Multiselect
A multiselect field collapses its selection to the first label plus a count. Set maxSelections to cap how many options can be picked; the rest disable once the cap is hit.
Manual layout
Pass children to lay the bar out yourself with FilterChips, FilterAddButton, FilterActiveCount, and FilterClearButton, which read the shared state from context.
Sizes
Use size to set the height of every pill in the bar.
Keyboard shortcut
Set shortcut to a key (here "f") to open the add-filter menu from the keyboard; a Kbd hint appears on the button. The key is ignored while typing or while a popup is open.
Press F (while not typing) to open the filter menu.
URL state with nuqs
Because Filters is controlled, syncing to the query string is just wiring value / onValueChange to a state hook. This uses nuqs to persist the filters in ?filters=. Mount NuqsAdapter once at your app root.
State outside the bar
Filters is FiltersProvider + FiltersBar composed. Use them separately when UI outside the flex row (a results count, saved views, an apply button) should read or mutate the same state through useFilters.
Accessibility
The field picker and value lists are Combobox instances, so type-to-search, arrow-key navigation, and selection follow the ARIA combobox pattern. The operator menu is a DropdownMenu radio group. Each pill is a group labeled with its full filter (for example, "Status is In progress"), and every segment is self-describing: the operator trigger is named "[field] operator: [operator]", value triggers and inputs are named "[field] value: ..." (including string affixes), and the remove control is "Remove [field] filter". Pressing Backspace or Delete on one of a pill's button segments removes the filter, and focus moves to the neighboring pill (or the add button) instead of dropping to the page.
The Backspace removal and focus rescue rely on the data-slot attributes (filter-chip, filter-chip-remove, filter-add); custom chip children should preserve those slots to keep the keyboard behavior.
API Reference
Filters is a custom component composed from Cubby UI primitives. It has no Base UI root of its own; the value and field pickers are Combobox and the operator menu is DropdownMenu.
Props
Filters
FiltersProvider + FiltersBar composed, for the common case. All props below except shortcut belong to the provider; shortcut and div props flow to the bar.
FiltersProvider
Owns filter state and provides it via context without rendering layout. Takes the state props from Filters (fields, value, defaultValue, onValueChange, size, allowDuplicateFields, labels) plus children.
FiltersBar
The flex row. Renders the default layout (FilterChips, FilterAddButton, FilterClearButton) unless children is passed. Takes shortcut and div props.
FilterClearButton
Clears every filter. Renders nothing while no filters are active, so compositions don't need to wrap it in a conditional.
FilterChips
Renders a FilterChip for every active filter, in order. Use it in children mode to place the chip list among your own chrome.
FilterChip
A single pill. Without children it renders the field, operator, value, and remove segments; pass children to compose the segments yourself.
FilterAddButton
The searchable field picker. Renders the dashed trigger and the popup.
Hooks
useFilters() exposes the bar's state and actions (filters, fieldsById, addFilter, updateFilter, removeFilter, clearAll) to anything rendered inside a FiltersProvider. It re-renders on every filter edit; performance-sensitive leaves can subscribe to useFiltersState() (just filters) or useFiltersActions() (config and actions, stable while a value is being typed) instead. useFilterChip() exposes the current pill's filter, field, and size inside a FilterChip.
State merging uses useControllableState, installed as a shared hook.
Types
FilterField
A discriminated union keyed on type. Every variant has id, label, and optional icon, operators, and disabledOperators. select and multiselect add options (multiselect also takes maxSelections); text and number take prefix / suffix; custom adds renderValue and optional defaultValue.
FilterValue
Helpers
createFilter
Creates a FilterValue with a stable id, the field's first operator, and a typed empty value. Pass a partial to override any of them.
patchFilter
Applies a partial update to a filter, reseeding the value when an operator change alters the value shape (e.g. eq to between).
asFilterValues
Coerces unknown JSON (e.g. a parsed URL param) into a FilterValue[], dropping entries with a malformed envelope. Pair it with the controlled API when restoring filters from untrusted sources; see the nuqs example.
formatFilterValue / describeFilter
formatFilterValue renders a filter's value as a human-readable string; describeFilter prepends the field and operator ("Status is In progress"). The pill's aria-label uses describeFilter, so custom UIs can stay consistent with it.
resolveOperators
Returns the operators available for a field, applying operators and disabledOperators.