React Filter Builder: AND / OR Chips and Saved Views
A React filter builder is the bar above a list where people add conditions one by one: City is Chicago, Balance is at least $5,000, Last invoice in the last 30 days. Each condition becomes a chip, the chips combine with AND or OR, and the count next to them says how many rows are left. It is a familiar pattern from issue trackers and spreadsheet-style databases, and it is what back office users ask for once a search box and a status dropdown stop being enough. This guide shows how to build one with the Wingo UI Filter Builder, how to wire it to a Data Table and to a server, and when a plain Filter Panel is the better choice.
What is a filter builder, and when do you need one?
A filter builder edits a small tree: a root group with a combinator (and or or) and a list of conditions, where each condition is a field, an operator and a value. A nested group adds parentheses, so "in Chicago and (overdue or balance over $5,000)" is one root AND with a condition and an OR group inside it.
You need one when the questions people ask about a list change from day to day and include numbers, dates or OR. Sales ops filtering clients, support filtering tickets, finance filtering invoices: they do not want a new hard-coded tab for every question. If your list only ever needs "status is X" and a search, the Data Table already does that with its column filters and quick filter chips, and a builder would be extra weight. Here is which filter UI component fits which job:
Does shadcn have a filter builder?
It does not. The shadcn Data Table guide (opens in a new tab) (checked November 6, 2026) adds one text input that filters the email column through TanStack Table and says you can add filters to other columns the same way. Faceted filters, AND / OR conditions and saved views are not part of it, so "shadcn filters" or a "shadcn data table filter" with conditions in practice means picking a registry item or writing your own.
If you search for "react query builder shadcn", you will land on React Query Builder (opens in a new tab). As of November 6, 2026 it ships compatibility packages for Ant Design, Chakra UI, Mantine, MUI and others, plus a shadcn registry item installed with npx shadcn add https://react-querybuilder.js.org/r/query-builder.json. Its QueryBuilder docs (opens in a new tab) set maxLevels to unlimited nesting by default and offer independent combinators between rules, and its export docs (opens in a new tab) list formatQuery targets such as SQL, Prisma, Drizzle, MongoDB, CEL and JsonLogic. That is the better tool when you need a full rule tree that you store and run as a query, for example in a reporting tool. It does not cover the list side: chips, a result count, saved views or a phone layout. That list side is what the rest of this guide is about.
How do you add a filter builder to a React list?
Install it with the CLI. It brings lib/filters (the filter model, evaluation and serialization) and the editors it uses:
Describe your fields once. Each field has an id (the row key, or an accessor), a label and a type: text, number, currency, date, select, multi-select or boolean. The type decides which operators the field offers and which editor opens: a text input, a number or money field, a calendar with quick ranges, or a searchable checklist.
applyFilter runs the same tree against your rows: text matches ignore case and accents, dates compare by local day, and incomplete conditions are skipped, so a half-typed chip never empties the list. previewCount lets the phone sheet say "Show 3 results" before anything is applied.
$ npx wingo-ui@latest add filter-builderWhich props matter most?
Six props decide how the builder behaves; the rest are appearance (size, radius, tone) and controlled state.
variant:chips(the default) puts every condition inline with its own editor popover.panelshows one Filters button with a count that opens a rows editor (one row per condition), and is where nested groups are easiest to edit.applyMode:instantcallsonValueChangeon every complete change (text after a 300ms pause).applykeeps a draft with Cancel and Apply, which is what you want when each change costs a request.allowOrandallowGroups: both default totrue. Turn OR off for audiences that never need it; the builder then stays a plain AND list. Groups go one level deep, which covers "A and (B or C)" without turning the UI into a tree editor.resultCountandloading: the count sits in a polite live region, so screen readers hear "24 results" after each change, andloadingshows a spinner while a server answers.maxChips: on desktop, chips past this number fold into a "+3" chip that opens the panel.labels: every string, English by default. It takes a partial object, so pass only the keys you translate;DEFAULT_FILTER_BUILDER_LABELSholds the full English set.
Fields can also limit their operators with operators: [...]. Do this when the server only supports some of them, so people cannot build a filter your backend silently ignores.
$ npx wingo-ui@latest add filter-builderHow do saved views work?
A view is a named filter: { id, name, filter, readOnly?, pinned? }. Pass them in views and the views menu lists them, pinned first, with All records on top. Picking one replaces the filter. Editing it afterwards marks it "(modified)" and offers Save changes or Save as new view.
Saving is yours to implement, because only your app knows where views live. Return the saved view from onSaveView and it becomes active. If the promise rejects, the builder keeps the name field open and announces the error, so nothing the user typed is lost. A rejected onDeleteView announces its own error and keeps the view in the menu.
serializeFilter (from @/lib/filters) turns the tree into a short URL-safe string, and parseFilter reads it back. The same string works for ?filter= links, so a teammate can paste a URL and see exactly the list you saw.
How do you connect it to a data table?
Keep the filter state above both components and pass the filtered rows down. The table keeps its own search, sorting, selection and phone cards; the builder decides which rows it gets.
Leave the table's per-column filter options off when the builder is there, so people have one place to filter instead of two that disagree. For lists of thousands of rows that stay in the browser, this is all you need. For the TanStack side of the table itself, see the TanStack Table v9 data table walkthrough.
How do you run the filter on the server?
lib/filters has no React in it, so the same parseFilter works in a route handler. For a database, walk the tree and turn each condition into a parameterized clause. The whitelist is the important part: field ids map to known columns, and anything unknown is dropped instead of interpolated.
On the client, send serializeFilter(filter) from onValueChange, set loading while the request runs and pass the total from the response as resultCount. Combine it with applyMode="apply" when the query is slow. Limit each field's operators to the cases your switch handles, so the UI never offers one the server drops. Also match the client's text rules: applyFilter ignores case and accents, while = in Postgres does neither, so use ILIKE, a citext column or unaccent() if the browser and server counts must agree.
How does it behave on a phone?
Below 768px the chips stay visible and scroll sideways, each with an × that removes it at once. Everything else moves into one bottom sheet behind a Filters button with the count: conditions as rows, and / or as a segmented control, and + Add filter drills into the field list and then the editor with a short slide. Nothing changes until the user taps "Show 24 results"; closing the sheet throws the draft away. That is deliberate: a popover anchored to a 32px chip would cover the list the user is trying to filter, and an instant filter behind a full-height sheet changes a list nobody can see. Touch targets grow to 44px on coarse pointers. Set responsive={false} only if your app is desktop-only.
When is a simple filter panel the better UI?
When the people filtering are shoppers browsing a catalog. A shopper looking for a used phone under $300 wants to tick Apple, drag a price range and see "Show 1,240 results"; they never think in operators or OR groups. The Filter Panel is built for that: a sidebar of facets that applies as you go on desktop (checkbox lists with counts, chips, price over a histogram, location with a radius, toggles) and a full-height sheet with a live count on phones.
$ npx wingo-ui@latest add filter-panelA good rule: if you can list every useful filter in advance and each has a handful of values, use the panel. If users need to invent their own questions and save them, use the builder. Some apps use both: the panel on the public catalog, the builder in the admin screen behind it.
What are the common pitfalls?
- Chips that filter nothing. A chip with an operator but no value looks active and is not. The builder removes an incomplete chip when its editor closes; if you build your own, do the same.
- Two filter systems on one screen. Column filters in the table header plus a builder above it give two answers to "why is this row hidden". Pick one.
- OR without parentheses. "Chicago and overdue or VIP" is ambiguous. Keep OR at the root or inside a visible group, and show the sentence (
describeFilterinlib/filterswrites it) where people can read it. - Silent server gaps. If the backend ignores an operator, people see wrong counts. Restrict
operatorsper field. - No count. Without "24 results" people cannot tell whether a filter did anything. Pass
resultCount, and0is a real answer.
For more guides like this one, browse the component guides category or start from the pillar on React form components, which uses the same field anatomy the editors here share.
Where to start
The Filter Builder, the Data Table and the Filter Panel are Pro components, included in every plan on the pricing page. The fastest way to judge the builder is the playground on its docs page: switch variant, applyMode and allowGroups and watch what changes. When it fits, install it with npx wingo-ui@latest add filter-builder and point fields at your own list.
FAQ
What is the difference between a filter builder and a query builder?
A query builder usually edits a full rule tree with unlimited nesting and exports it to SQL, MongoDB or another query language, so it suits admin tools that store queries. A filter builder is the lighter, list-facing version: chips for each condition, AND / OR, one level of groups, saved views and a result count next to the list.
Does shadcn/ui have a filter builder component?
No. As of November 2026 the shadcn Data Table guide shows a text input that filters one column through TanStack Table and points to the TanStack docs for more. AND / OR conditions, nested groups and saved views are left to you or to a third-party registry item.
How do I store a filter in the URL?
Serialize the filter tree into a short URL-safe string and put it in a query parameter such as ?filter=. In Wingo UI, serializeFilter(filter) from lib/filters writes it and parseFilter(text) reads it back, returning null for a stale or hand-edited link so the page can fall back to no filter.
Should filters apply instantly or with an Apply button?
Apply instantly when filtering is cheap, such as a list already in the browser, so people see the effect of each change. Use an Apply button when every change costs a server request or a slow query, and always keep a draft on phones, where the filter editor covers the list.
How do I filter a React table on the server with AND / OR conditions?
Send the filter tree to an endpoint, walk it recursively, and turn each condition into a parameterized SQL clause joined by AND or OR, with groups in parentheses. Map field ids to columns through a whitelist so a client can never inject a column name or an operator you do not support.
- Filter builder
- Query builder
- Data table
- Saved views
- React
