# Filter (/product/blocks/filter)



The `filter` block turns a list of field definitions into a working filter sidebar, a sort picker and a quick search, and hands you back the criteria the user chose. It depends on no other block.

## Live

Filter the eight products below by name, category, price or whether they are for sale, and sort by name or price. Your choices go into the page's URL, so the state survives a reload and a shared link carries it.

_The filter block renders live on this page._

## Defs, not JSX

A surface declares the complete set of filters up front, as data:

```tsx
const filters: FilterDefs = [
  { name: "name", type: "text", label: "Name" },
  {
    name: "category",
    type: "multiselect",
    label: "Category",
    items: categories,
  },
  {
    name: "price",
    type: "number",
    control: "slider",
    label: "Price",
    min: 0,
    max: 10000,
  },
  { name: "active", type: "boolean", label: "For sale" },
];
```

The whole array has to exist before the first render, because it is what the block deserialises the URL against and what it hydrates the initial state from. A registry that collected filters as their components mounted would learn the set too late, and the page would flicker through an empty state on every load.

`type` is the kind of data, and it decides the operators and how a value is compared. `control` decides the input, and defaults from the type — so a number is a pair of boxes unless you ask for a slider, and a multi-select is a checklist unless you ask for a combobox. An empty `items` hides its field, which is how a storefront's brand filter disappears on a catalogue that has no brands, without the page testing for it.

A `label` is looked up as a copy path and falls through to itself when it is not one, so a label that only exists at runtime — a category name out of your database — passes straight through.

## What you get back

`useFiltersContext()` returns the state and the actions. The state carries the active filters, the sort list, the search text and its debounced twin; the actions add, update, remove and clear. Two matchers, `doesPassFilters` and `doesPassSearch`, apply the same semantics in the browser that a server query would apply in the database, so a small list can be filtered where it stands and a large one can send the criteria on to be paginated.

## The parts

`FilterSidebar` is the panel, `FilterFields` renders one collapsible row per definition, and `FilterSidebarTrigger` opens and closes it. `SortMenu` and `SortToggle` drive the sort list. The individual controls — text, select, async select, number, slider, date, switch, toggle — carry no chrome of their own, so the sidebar row and the pill bar are made of the same ones. Whether a control takes one value or several is read from the filter's type, never passed in.

## Where the state lives

`FilterPersistenceProvider` decides that. It writes to the URL, so a filtered view is a link you can send someone, and optionally to the device as well, under the entity name you put above it. When the state grows too large for a URL, it stays on the device and tells the user. The URL half is not namespaced, so one page carries one URL-persisted surface.

- [All blocks](/product/blocks) — What a block is, what it may import, and how your data reaches it.

- [Form](/product/blocks/form) — Fields, validation, the surface shells and the submit contract.
