const [values, setValues] = useState<FilterValues>({});
<Filter.Root
definitions={definitions}
values={values}
onValuesChange={setValues}
searchValue={search}
onSearchChange={setSearch}
resultCount={total}
/>;FilterValues never holds an empty filter, so Object.keys(values).length is
the active count and the record round-trips to a query string unchanged.
Two ways to commit
The default, mode="apply", puts a search-shaped trigger in the bar and opens a
dialog whose edits are a draft: nothing reaches onValuesChange until Apply,
and dismissing discards. Pick it when every change costs a request you would
rather not make twice.
mode="live" puts a real search input in the bar beside a "Filters" button,
and the side sheet behind it applies each change as it happens — a tap at
once, typing after a 300 ms pause — so the sheet can stay open while the rows
shift behind it. Pick it for an admin list, where narrowing and reading
alternate.
<Filter.Root
mode="live"
definitions={definitions}
values={values}
onValuesChange={setValues}
searchValue={search}
onSearchChange={setSearch}
resultCount={total}
/>;For a rail beside the list instead of an overlay, keep the bar and render
Filter.Dock yourself, with panelRenderedElsewhere.
API
Filter.Root
| Prop | Type | Default | |
|---|---|---|---|
definitions | readonly FilterDefinition[] | required | The filters this screen offers, in the order the editor shows them. The same array drives the editor, the chips and your own query layer. |
onValuesChange | (values: Readonly<Record<string, FilterValue>>) => void | required | Called with the new applied record, and immediately when a chip is
removed. In apply mode once per Apply — drafts inside the editor never
reach it; in live mode as the filters change, typing after a 300 ms
pause. Either way it is safe to use as a fetch key. |
values | Readonly<Record<string, FilterValue>> | required | The applied filters, keyed by definition.key. Never holds an empty
filter — clearing one deletes its key — so Object.keys(values).length is
the active count and the record round-trips to a query string unchanged. |
fullWidthTrigger | boolean | false | Let the trigger (or the search input) fill the row instead of capping at
max-w-md. The cap suits wide tables; a narrow content column wants the
bar flush with the cards below it. |
hideChips | boolean | false | Drop the chips row. For filters that are standing preferences rather than ad-hoc facets: the active count on the trigger already says they apply, and chips would restate them forever. |
labels | Partial<FilterLabels> | — | Overrides for the strings the surface shows. Merged over
defaultFilterLabels (English), so a partial is enough. |
locale | string | — | BCP 47 tag used to format the numbers, money and dates in the chips and the editor. |
mode | "apply" | "live" | "apply" | How the editor commits. apply (the default): a search-field-shaped
trigger opens a dialog whose edits are a draft committed on Apply, search
included. live: a real search input sits in the bar beside a "Filters"
button, and the side sheet behind it applies each change as it happens. |
onOpenChange | ((open: boolean) => void) | — | |
onSearchChange | ((value: string) => void) | — | Called on every keystroke (in apply mode, on Apply). Debounce your query. |
open | boolean | — | Drive the editor from outside, for a screen with a second way in (an empty state whose call to action is "adjust your filters"). Omit both to let the bar own the state. |
panelRenderedElsewhere | boolean | false | Leave the editor to the caller: a list that docks Filter.Dock beside its
table keeps only the bar here, and the bar's button toggles the rail
through open/onOpenChange — so the two never both draw the fields. |
resultCount | number | — | How many rows the current filters matched, shown next to the trigger. Omit
it while the count is unknown rather than passing 0. |
searchValue | string | — | Omit both search props for a list that filters without free text. |
exit | Exit | — | Inside a <Presence>: plays this child's way out before it unmounts. |
Plus every <div> prop. Source: filter/components/filter-bar.tsx.
onValuesChange fires on apply (or, live, once the edit settles) and on chip
removal, never on a keystroke inside the editor, so it is safe to use directly
as a fetch key. onSearchChange fires per keystroke in live mode, so debounce
that one yourself.
hideChips drops the chips row for filters that are standing preferences
rather than ad-hoc facets; open and onOpenChange let an empty state's
"adjust your filters" button open the same editor.
Filter.Panel
| Prop | Type | Default | |
|---|---|---|---|
definitions | readonly FilterDefinition[] | required | The filters to edit, in the order they are rendered. |
onOpenChange | (open: boolean) => void | required | Called when the overlay is dismissed or applied. Dismissing discards the draft. |
onValuesChange | (values: Readonly<Record<string, FilterValue>>) => void | required | Called once, on apply, with the committed record. |
open | boolean | required | Whether the overlay is open. Controlled; pair it with onOpenChange. |
values | Readonly<Record<string, FilterValue>> | required | The applied record the draft starts from, re-read each time the panel opens. |
labels | Partial<FilterLabels> | — | Overrides for the strings the overlay shows, including the apply and clear buttons, merged over the surrounding filter root's (English defaults at the outermost level). |
locale | string | — | BCP 47 tag used to format the numbers, money and dates; inherited when omitted. |
onSearchChange | ((value: string) => void) | — | Called on every keystroke — search is not part of the apply-on-commit draft. |
searchValue | string | — | Free-text search, kept beside the filters when the list has one. |
Plus the DOM props of the element it renders. Source: filter/components/filter-panel.tsx.
The dialog mode="apply" opens, exposed for the case where you want its
draft-then-apply behaviour behind a trigger of your own. Pair it with
Filter.Trigger, or with any button you like — it
only needs open and onOpenChange. For the editor with no overlay and no
commit step, use Filter.Form.
Filter.Sheet
| Prop | Type | Default | |
|---|---|---|---|
definitions | readonly FilterDefinition[] | required | The filters to edit, in the order they are rendered. |
onOpenChange | (open: boolean) => void | required | Called when the sheet is dismissed or closed from its footer. Edits are already applied. |
onValuesChange | (values: Readonly<Record<string, FilterValue>>) => void | required | Called as the filters change: at once for a tap (an option, a preset, a place), after a 300 ms pause for typing. Safe to use as a fetch key. |
open | boolean | required | Whether the sheet is open. Controlled; pair it with onOpenChange. |
values | Readonly<Record<string, FilterValue>> | required | The applied record. Changes made elsewhere (a removed chip) flow back in. |
labels | Partial<FilterLabels> | — | Overrides for the strings the sheet shows, merged over the surrounding filter root's (English defaults at the outermost level). |
locale | string | — | BCP 47 tag used to format the numbers, money and dates; inherited when omitted. |
Plus the DOM props of the element it renders. Source: filter/components/filter-sheet.tsx.
The side sheet (a bottom drawer on mobile) mode="live" opens. Edits apply as
they happen, so its footer has Close rather than Apply. From six filters on, a
search box at the top finds one by name, accent- and case-blind.