# React Filter Builder: AND/OR-Chips, gespeicherte Ansichten

> Bau einen React Filter Builder für jede Liste: Chips pro Bedingung, AND/OR-Gruppen, gespeicherte Ansichten, Live-Trefferzahl, Bottom Sheet und Server-Query.

- Autor: [Serban Rusu](https://wingo-ui.com/blog/authors/serban), Founder of Wingo UI
- Veröffentlicht: 6. Nov. 2026
- Kategorie: [Komponenten-Guides](https://wingo-ui.com/de/blog/category/components)
- Lesezeit: 11 Min.
- Kanonische URL: https://wingo-ui.com/de/blog/react-filter-builder
- Englische Fassung: https://wingo-ui.com/blog/react-filter-builder

## Kurz gesagt

Mit einem React Filter Builder kombinieren Nutzer Bedingungen (Feld, Operator, Wert) mit AND / OR, verschachteln eine Gruppe, speichern das Ergebnis als benannte Ansicht und sehen schon beim Bearbeiten, wie viele Zeilen passen. Er gehört in Backoffice-Listen, in denen Power-User Fragen stellen wie „überfällig oder über $5,000, in Chicago“. Für Kunden, die durch einen Katalog stöbern, ist ein Filter Panel mit Facetten, Checkboxen und Preisbereich die bessere UI.

Ein React Filter Builder ist die Leiste über einer Liste, in der Nutzer Bedingungen nacheinander hinzufügen: Stadt ist Chicago, Saldo mindestens $5,000, letzte Rechnung in den letzten 30 Tagen. Jede Bedingung wird zu einem Chip, die Chips werden mit AND oder OR verknüpft, und die Zahl daneben sagt, wie viele Zeilen übrig sind. Das Muster kennst du aus Issue-Trackern und tabellenartigen Datenbanken, und genau danach fragen Backoffice-Nutzer, sobald ein Suchfeld und ein Status-Dropdown nicht mehr reichen. Dieser Guide zeigt, wie du einen mit dem [Filter Builder](https://wingo-ui.com/components/filter-builder) von Wingo UI baust, wie du ihn an eine [Data Table](https://wingo-ui.com/components/data-table) und an einen Server anbindest und wann ein schlichtes [Filter Panel](https://wingo-ui.com/components/filter-panel) die bessere Wahl ist.

## Was ist ein Filter Builder, und wann brauchst du einen?

Ein Filter Builder bearbeitet einen kleinen Baum: eine Wurzelgruppe mit einem Kombinator (`and` oder `or`) und einer Liste von Bedingungen. Jede Bedingung besteht aus Feld, Operator und Wert. Eine verschachtelte Gruppe bringt Klammern ins Spiel, „in Chicago und (überfällig oder Saldo über $5,000)“ ist also ein AND an der Wurzel mit einer Bedingung und einer OR-Gruppe darin.

Du brauchst einen, wenn sich die Fragen an eine Liste von Tag zu Tag ändern und Zahlen, Datumswerte oder ein OR enthalten. Sales Ops filtert Kunden, der Support Tickets, die Buchhaltung Rechnungen: Niemand will für jede Frage einen neuen, fest verdrahteten Tab. Braucht deine Liste nur „Status ist X“ und eine Suche, kann die [Data Table](https://wingo-ui.com/components/data-table) das schon mit ihren Spaltenfiltern und Quick-Filter-Chips, und ein Builder wäre unnötiger Ballast. Welche Filter-UI für welchen Zweck passt:

| Nutzer sollen | Nimm |
| --- | --- |
| suchen und ein oder zwei Status umschalten | Suche, Spaltenfilter und `quickFilters` der Data Table |
| einen Katalog nach Facetten durchstöbern (Preis, Marke, Zustand, Entfernung) | Filter Panel |
| beliebige Felder mit Operatoren, AND / OR und gespeicherten Ansichten kombinieren | Filter Builder |
| eine Abfrage speichern, die eine Automation steuert („wenn X, dann Y“) | [Rule Builder](https://wingo-ui.com/components/rule-builder) |

## Hat shadcn einen Filter Builder?

Nein. Der [Data-Table-Guide von shadcn](https://ui.shadcn.com/docs/components/data-table) (geprüft am 6. November 2026) fügt ein Textfeld hinzu, das die E-Mail-Spalte über TanStack Table filtert, und sagt, dass du andere Spalten auf dieselbe Weise filtern kannst. Facettenfilter, AND / OR-Bedingungen und gespeicherte Ansichten gehören nicht dazu. „shadcn filters“ oder ein „shadcn data table filter“ mit Bedingungen heißt in der Praxis also: ein Registry-Item auswählen oder selbst schreiben.

Wenn du nach „react query builder shadcn“ suchst, landest du bei [React Query Builder](https://react-querybuilder.js.org/docs/compat). Stand 6. November 2026 liefert das Projekt Kompatibilitätspakete für Ant Design, Chakra UI, Mantine, MUI und weitere, dazu ein shadcn-Registry-Item, das du mit `npx shadcn add https://react-querybuilder.js.org/r/query-builder.json` installierst. Laut [QueryBuilder-Doku](https://react-querybuilder.js.org/docs/components/querybuilder) steht `maxLevels` standardmäßig auf unbegrenzter Verschachtelung, und zwischen den Regeln sind unabhängige Kombinatoren möglich. Die [Export-Doku](https://react-querybuilder.js.org/docs/utils/export) nennt Ziele für `formatQuery` wie SQL, Prisma, Drizzle, MongoDB, CEL und JsonLogic. Das ist das bessere Werkzeug, wenn du einen kompletten Regelbaum brauchst, den du speicherst und als Abfrage ausführst, etwa in einem Reporting-Tool. Was direkt an der Liste passiert, deckt es nicht ab: Chips, Trefferzahl, gespeicherte Ansichten oder ein Smartphone-Layout. Um genau diese Seite geht es im Rest dieses Guides.

## Wie fügst du einer React-Liste einen Filter Builder hinzu?

Installier ihn mit der CLI. Sie bringt `lib/filters` (Filtermodell, Auswertung und Serialisierung) und die Editoren mit, die er nutzt:

```bash
npx wingo-ui@latest add filter-builder
```

Beschreib deine Felder einmal. Jedes Feld hat eine `id` (den Schlüssel in der Zeile oder einen `accessor`), ein `label` und einen `type`: `text`, `number`, `currency`, `date`, `select`, `multi-select` oder `boolean`. Der Typ bestimmt, welche Operatoren das Feld anbietet und welcher Editor aufgeht: ein Textfeld, ein Zahlen- oder Betragsfeld, ein Kalender mit Schnellauswahl oder eine durchsuchbare Checkliste.

```tsx
"use client"

import { useMemo, useState } from "react"
import { CalendarDays, MapPin, Wallet } from "lucide-react"
import { FilterBuilder, type FilterBuilderField, type FilterGroup, type FilterView } from "@/components/ui/filter-builder"
import { applyFilter, createFilterCondition, createFilterGroup } from "@/lib/filters"

type Client = { id: string; name: string; city: string; status: string; balance: number; lastInvoice: string | null }

const FIELDS: FilterBuilderField<Client>[] = [
  { id: "name", label: "Kunde", type: "text", group: "Kunde" },
  {
    id: "city",
    label: "Stadt",
    type: "select",
    icon: <MapPin />,
    group: "Kunde",
    options: [
      { value: "New York", label: "New York" },
      { value: "Chicago", label: "Chicago" },
      { value: "Austin", label: "Austin" },
    ],
  },
  {
    id: "status",
    label: "Status",
    type: "select",
    group: "Kunde",
    options: [
      { value: "active", label: "Aktiv", color: "#15803D" },
      { value: "overdue", label: "Überfällig", color: "#DC2626" },
    ],
  },
  { id: "balance", label: "Saldo", type: "currency", currency: "USD", icon: <Wallet />, group: "Finanzen" },
  { id: "lastInvoice", label: "Letzte Rechnung", type: "date", icon: <CalendarDays />, group: "Finanzen" },
]

const VIEWS: FilterView[] = [
  {
    id: "overdue",
    name: "Überfällig",
    readOnly: true,
    pinned: true,
    filter: createFilterGroup("and", [createFilterCondition(FIELDS[2], { operator: "is", value: "overdue" })]),
  },
]

export function ClientFilters({ clients }: { clients: Client[] }) {
  const [filter, setFilter] = useState<FilterGroup>(() => createFilterGroup("and"))
  const rows = useMemo(() => applyFilter(clients, filter, FIELDS), [clients, filter])

  return (
    <FilterBuilder
      fields={FIELDS}
      value={filter}
      onValueChange={setFilter}
      views={VIEWS}
      resultCount={rows.length}
      previewCount={(draft) => applyFilter(clients, draft, FIELDS).length}
      locale="en-US"
      shortcut="shift+f"
      labels={{ allRecords: "Alle Kunden" }}
    />
  )
}
```

`applyFilter` wendet denselben Baum auf deine Zeilen an: Textvergleiche ignorieren Groß- und Kleinschreibung sowie Akzente, Datumswerte werden nach lokalem Tag verglichen, und unvollständige Bedingungen werden übersprungen. Ein halb ausgefüllter Chip leert die Liste also nie. Mit `previewCount` zeigt das Sheet auf dem Smartphone schon „Show 3 results“, bevor irgendetwas angewendet ist.

> Live-Demo (Filter Builder): Die Demo startet mit den Chips „Status is Active“ und „Balance at least $5,000“. Drück auf „+ Filter“ (oder Shift+F), wähl „City“ und hak Chicago an: Der Chip erscheint mit geöffnetem Editor, und die Zahl aktualisiert sich. Klick auf das „and“ zwischen den ersten beiden Chips, um den ganzen Filter auf „or“ umzustellen. Probier sie unter [Filter Builder](https://wingo-ui.com/components/filter-builder) aus und installier sie mit `npx wingo-ui@latest add filter-builder`.

## Welche Props sind am wichtigsten?

Sechs Props bestimmen, wie sich der Builder verhält; der Rest betrifft das Aussehen (`size`, `radius`, `tone`) und den kontrollierten State.

- `variant`: `chips` (der Standard) setzt jede Bedingung inline mit einem eigenen Editor-Popover. `panel` zeigt einen einzigen Filters-Button mit Zähler, der einen Editor mit Zeilen öffnet (eine Zeile pro Bedingung). Dort lassen sich verschachtelte Gruppen am leichtesten bearbeiten.
- `applyMode`: `instant` ruft `onValueChange` bei jeder vollständigen Änderung auf (Text nach 300ms Pause). `apply` hält einen Entwurf mit Cancel und Apply. Das willst du, wenn jede Änderung einen Request kostet.
- `allowOr` und `allowGroups`: Beide stehen standardmäßig auf `true`. Schalte OR für Zielgruppen ab, die es nie brauchen; der Builder bleibt dann eine schlichte AND-Liste. Gruppen gehen eine Ebene tief. Das reicht für „A und (B oder C)“, ohne dass aus der UI ein Baumeditor wird.
- `resultCount` und `loading`: Die Zahl steht in einer Live-Region mit `aria-live` polite, Screenreader hören nach jeder Änderung also „24 results“. `loading` zeigt einen Spinner, während der Server antwortet.
- `maxChips`: Auf dem Desktop wandern Chips über dieser Zahl in einen „+3“-Chip, der das Panel öffnet.
- `labels`: alle Texte, standardmäßig Englisch. Die Prop nimmt ein Teilobjekt, du gibst also nur die Schlüssel mit, die du übersetzt; `DEFAULT_FILTER_BUILDER_LABELS` enthält den kompletten englischen Satz.

Felder können ihre Operatoren außerdem mit `operators: [...]` einschränken. Mach das, wenn der Server nur einen Teil davon unterstützt, damit niemand einen Filter baut, den dein Backend stillschweigend ignoriert.

> Live-Demo (Filter Builder): Die Panel-Variante: Öffne „Filters“, füg eine Bedingung hinzu und bau dann mit „Add group“ (Balance mindestens $5,000 oder Last invoice in den letzten 7 Tagen). Probier sie unter [Filter Builder](https://wingo-ui.com/components/filter-builder) aus und installier sie mit `npx wingo-ui@latest add filter-builder`.

## Wie funktionieren gespeicherte Ansichten?

Eine Ansicht ist ein benannter Filter: `{ id, name, filter, readOnly?, pinned? }`. Gib die Ansichten in `views` mit, und das Ansichtsmenü listet sie auf, angepinnte zuerst, ganz oben „All records“. Wer eine auswählt, ersetzt damit den Filter. Wird sie danach bearbeitet, steht „(modified)“ dahinter, und das Menü bietet Save changes oder Save as new view an.

Das Speichern implementierst du selbst, denn nur deine App weiß, wo Ansichten (Saved Views) liegen. Gib die gespeicherte Ansicht aus `onSaveView` zurück, dann wird sie aktiv. Wird das Promise abgelehnt, lässt der Builder das Namensfeld offen und meldet den Fehler, damit nichts verloren geht, was der Nutzer getippt hat. Ein abgelehntes `onDeleteView` meldet seinen eigenen Fehler und lässt die Ansicht im Menü.

```tsx
import { serializeFilter } from "@/lib/filters"

export function ClientFiltersWithViews({ clients }: { clients: Client[] }) {
  const [filter, setFilter] = useState<FilterGroup>(() => createFilterGroup("and"))
  const [views, setViews] = useState<FilterView[]>(VIEWS)
  const [activeViewId, setActiveViewId] = useState<string | null>(null)
  const rows = useMemo(() => applyFilter(clients, filter, FIELDS), [clients, filter])

  return (
    <FilterBuilder
      fields={FIELDS}
      value={filter}
      onValueChange={setFilter}
      resultCount={rows.length}
      views={views}
      activeViewId={activeViewId}
      onActiveViewChange={setActiveViewId}
      onSaveView={async ({ name, filter: draft, id }) => {
        const res = await fetch("/api/views", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ id, name, filter: serializeFilter(draft) }),
        })
        if (!res.ok) throw new Error("Die Ansicht konnte nicht gespeichert werden")
        const saved: FilterView = { id: (await res.json()).id, name, filter: draft }
        setViews((list) => [...list.filter((v) => v.id !== saved.id), saved])
        return saved
      }}
      onDeleteView={async (viewId) => {
        const res = await fetch(`/api/views/${viewId}`, { method: "DELETE" })
        if (!res.ok) throw new Error("Die Ansicht konnte nicht gelöscht werden")
        setViews((list) => list.filter((v) => v.id !== viewId))
      }}
    />
  )
}
```

`serializeFilter` (aus `@/lib/filters`) macht aus dem Baum einen kurzen, URL-sicheren String, und `parseFilter` liest ihn wieder ein. Derselbe String funktioniert für Links mit `?filter=`. So fügt jemand aus deinem Team die URL ein und sieht genau die Liste, die du gesehen hast.

## Wie verbindest du ihn mit einer Data Table?

Halte den Filter-State oberhalb beider Komponenten und gib die gefilterten Zeilen nach unten weiter. Die Tabelle behält ihre eigene Suche, Sortierung, Auswahl und die Karten auf dem Smartphone; der Builder entscheidet, welche Zeilen sie bekommt.

```tsx
import { DataTable, createColumns } from "@/components/ui/data-table"

const col = createColumns<Client>()
const columns = col([
  col.accessor("name", { header: "Kunde" }),
  col.accessor("city", { header: "Stadt" }),
  col.accessor("balance", { header: "Saldo", type: "currency", footer: "sum" }),
  col.accessor("lastInvoice", { header: "Letzte Rechnung", type: "relative" }),
])

export function ClientsScreen({ clients }: { clients: Client[] }) {
  const [filter, setFilter] = useState<FilterGroup>(() => createFilterGroup("and"))
  const rows = useMemo(() => applyFilter(clients, filter, FIELDS), [clients, filter])

  return (
    <div className="flex flex-col gap-3">
      <FilterBuilder fields={FIELDS} value={filter} onValueChange={setFilter} resultCount={rows.length} locale="en-US" />
      <DataTable data={rows} columns={columns} caption="Kunden" locale="en-US" searchPlaceholder="Kunden suchen" />
    </div>
  )
}
```

Lass die `filter`-Optionen pro Spalte in der Tabelle weg, wenn der Builder da ist. So gibt es einen Ort zum Filtern statt zwei, die sich widersprechen. Für Listen mit Tausenden Zeilen, die im Browser bleiben, ist das alles. Für die TanStack-Seite der Tabelle selbst lies den [Walkthrough zur Data Table mit TanStack Table v9](https://wingo-ui.com/de/blog/tanstack-table-v9-data-table).

## Wie führst du den Filter auf dem Server aus?

In `lib/filters` steckt kein React, also funktioniert dasselbe `parseFilter` auch in einem Route Handler. Für eine Datenbank läufst du durch den Baum und machst aus jeder Bedingung eine parametrisierte Klausel. Der wichtige Teil ist die Whitelist: Feld-IDs werden auf bekannte Spalten abgebildet, und alles Unbekannte fliegt raus, statt in den SQL-String zu wandern.

```ts
// app/api/clients/route.ts
import { isFilterGroup, parseFilter, type FilterCondition, type FilterGroup } from "@/lib/filters"

const COLUMNS: Record<string, string> = { name: "name", city: "city", status: "status", balance: "balance", lastInvoice: "last_invoice" }

function conditionSql(c: FilterCondition, params: unknown[]): string | null {
  const column = COLUMNS[c.field]
  if (!column) return null
  const p = (value: unknown) => {
    params.push(value)
    return `$${params.length}`
  }
  const v = c.value
  switch (c.operator) {
    case "contains":
      return typeof v === "string" && v ? `${column} ILIKE ${p(`%${v.replace(/[%_\\]/g, "\\$&")}%`)}` : null
    case "is":
      return typeof v === "string" ? `${column} = ${p(v)}` : null
    case "any-of":
      return Array.isArray(v) && v.length ? `${column} = ANY(${p(v)})` : null
    case "gte":
      return typeof v === "number" ? `${column} >= ${p(v)}` : null
    case "lte":
      return typeof v === "number" ? `${column} <= ${p(v)}` : null
    case "in-last":
      // heute plus die v - 1 Tage davor, dasselbe Fenster wie in applyFilter
      return typeof v === "number" ? `${column} >= current_date - (${p(v)}::int - 1)` : null
    case "is-empty":
      return `${column} IS NULL`
    default:
      return null
  }
}

function toWhere(group: FilterGroup, params: unknown[]): string {
  const parts: string[] = []
  for (const node of group.conditions) {
    const sql = isFilterGroup(node) ? toWhere(node, params) : conditionSql(node, params)
    if (sql) parts.push(isFilterGroup(node) ? `(${sql})` : sql)
  }
  return parts.join(group.combinator === "or" ? " OR " : " AND ")
}

export async function GET(request: Request) {
  const filter = parseFilter(new URL(request.url).searchParams.get("filter"))
  const params: unknown[] = []
  const where = filter ? toWhere(filter, params) : ""
  // `SELECT ... FROM clients ${where ? `WHERE ${where}` : ""}` mit params über deinen Postgres-Client ausführen
  return Response.json({ where, params })
}
```

Auf dem Client schickst du `serializeFilter(filter)` aus `onValueChange`, setzt `loading`, solange der Request läuft, und gibst die Gesamtzahl aus der Antwort als `resultCount` weiter. Kombinier das mit `applyMode="apply"`, wenn die Abfrage langsam ist. Beschränk die `operators` jedes Felds auf die Fälle, die dein `switch` behandelt, damit die UI nie einen anbietet, den der Server verwirft. Achte außerdem darauf, dass Client und Server Text gleich vergleichen: `applyFilter` ignoriert Groß- und Kleinschreibung sowie Akzente, `=` in Postgres tut beides nicht. Nimm also `ILIKE`, eine `citext`-Spalte oder `unaccent()`, wenn die Zahlen in Browser und Server übereinstimmen müssen.

## Wie verhält er sich auf dem Smartphone?

Unter 768px bleiben die Chips sichtbar und scrollen seitlich, jeder mit einem ×, das ihn sofort entfernt. Alles andere zieht in ein einziges Bottom Sheet hinter einem Filters-Button mit Zähler: Bedingungen als Zeilen, and / or als Segmented Control, und „+ Add filter“ führt mit einer kurzen Slide-Animation erst in die Feldliste und dann in den Editor. Nichts ändert sich, bis der Nutzer auf „Show 24 results“ tippt; wer das Sheet schließt, verwirft den Entwurf. Das ist Absicht: Ein Popover an einem 32px hohen Chip würde genau die Liste verdecken, die gefiltert werden soll, und ein Sofortfilter hinter einem Sheet in voller Höhe ändert eine Liste, die niemand sieht. Bei Touch-Bedienung wachsen die Touch-Ziele auf 44px. Setz `responsive={false}` nur, wenn deine App nur auf dem Desktop läuft.

## Wann ist ein einfaches Filter Panel die bessere UI?

Wenn die Leute, die filtern, Kunden sind, die durch einen Katalog stöbern. Wer ein gebrauchtes Smartphone unter $300 sucht, will Apple anhaken, einen Preisbereich ziehen und „Show 1,240 results“ sehen; in Operatoren oder OR-Gruppen denkt dabei niemand. Dafür ist das [Filter Panel](https://wingo-ui.com/components/filter-panel) gebaut: eine Seitenleiste mit Facetten, die auf dem Desktop jede Änderung direkt anwendet (Checkbox-Listen mit Zählern, Chips, Preis über einem Histogramm, Ort mit Radius, Schalter), und auf dem Smartphone ein Sheet in voller Höhe mit Live-Zähler.

> Live-Demo (Filter panel): Hak eine Marke an, zieh die Preisregler und schalte „Delivery only“ um: Die Seitenleiste wendet jede Änderung an, und auf dem Smartphone öffnen sich dieselben Filter als Sheet mit einem „Show results“-Button samt Zähler. Probier sie unter [Filter panel](https://wingo-ui.com/components/filter-panel) aus und installier sie mit `npx wingo-ui@latest add filter-panel`.

Eine gute Faustregel: Wenn du jeden sinnvollen Filter vorab aufzählen kannst und jeder nur eine Handvoll Werte hat, nimm das Panel. Wenn Nutzer sich eigene Fragen ausdenken und speichern sollen, nimm den Builder. Manche Apps nutzen beides: das Panel im öffentlichen Katalog, den Builder im Admin-Bereich dahinter.

## Was sind die typischen Fallstricke?

- **Chips, die nichts filtern.** Ein Chip mit Operator, aber ohne Wert sieht aktiv aus und ist es nicht. Der Builder entfernt einen unvollständigen Chip, sobald sein Editor zugeht; wenn du selbst einen baust, mach es genauso.
- **Zwei Filtersysteme auf einem Screen.** Spaltenfilter im Tabellenkopf plus ein Builder darüber geben zwei Antworten auf die Frage „Warum ist diese Zeile ausgeblendet?“. Entscheide dich für eins.
- **OR ohne Klammern.** „Chicago und überfällig oder VIP“ ist mehrdeutig. Halte OR an der Wurzel oder in einer sichtbaren Gruppe, und zeig den Filter als Satz (`describeFilter` in `lib/filters` schreibt ihn) dort, wo man ihn lesen kann.
- **Stille Lücken im Server.** Ignoriert das Backend einen Operator, sehen Nutzer falsche Zahlen. Schränk `operators` pro Feld ein.
- **Keine Trefferzahl.** Ohne „24 results“ weiß niemand, ob ein Filter überhaupt etwas bewirkt hat. Gib `resultCount` mit, und `0` ist eine echte Antwort.

Mehr Guides wie diesen findest du in der [Kategorie Komponenten-Guides](https://wingo-ui.com/de/blog/category/components), oder du startest mit dem Pillar-Artikel zu [React-Formularkomponenten](https://wingo-ui.com/de/blog/react-form-components-guide). Er nutzt dieselbe Feld-Anatomie wie die Editoren hier.

## Wo du anfängst

Filter Builder, Data Table und Filter Panel sind Pro-Komponenten und in jedem Plan auf der [Preisseite](https://wingo-ui.com/de/pricing) enthalten. Am schnellsten beurteilst du den Builder im Playground auf seiner [Doku-Seite](https://wingo-ui.com/components/filter-builder): Stell `variant`, `applyMode` und `allowGroups` um und schau, was sich ändert. Wenn er passt, installier ihn mit `npx wingo-ui@latest add filter-builder` und pass `fields` an deine eigene Liste an.

## Komponenten in diesem Artikel

- [Filter Builder](https://wingo-ui.com/components/filter-builder) (Pro): A React filter builder for any list: condition chips, AND / OR, nested groups, saved views and a live count, with one bottom sheet on phones. Installation: `npx wingo-ui@latest add filter-builder`
- [Data Table](https://wingo-ui.com/components/data-table) (Pro): A React data table on TanStack Table: search, filters, sorting, bulk actions, pinning, resizing, virtualization and cards on phones. Installation: `npx wingo-ui@latest add data-table`
- [Filter panel](https://wingo-ui.com/components/filter-panel) (Pro): A React filter panel for faceted search: a sidebar that applies as you go on desktop, a full-height sheet with a live count on phones. Installation: `npx wingo-ui@latest add filter-panel`

## Häufige Fragen

### Was ist der Unterschied zwischen einem Filter Builder und einem Query Builder?

Ein Query Builder bearbeitet meist einen kompletten Regelbaum mit beliebiger Verschachtelung und exportiert ihn nach SQL, MongoDB oder in eine andere Abfragesprache. Er passt also zu Admin-Tools, die Abfragen speichern. Ein Filter Builder ist die leichtere Variante direkt an der Liste: ein Chip pro Bedingung, AND / OR, eine Ebene Gruppen, gespeicherte Ansichten und eine Trefferzahl neben der Liste.

### Hat shadcn/ui eine Filter-Builder-Komponente?

Nein. Stand November 2026 zeigt der Data-Table-Guide von shadcn ein Textfeld, das über TanStack Table eine Spalte filtert, und verweist für alles Weitere auf die TanStack-Doku. AND / OR-Bedingungen, verschachtelte Gruppen und gespeicherte Ansichten bleiben dir oder einem Registry-Item von Dritten überlassen.

### Wie speichere ich einen Filter in der URL?

Serialisier den Filterbaum zu einem kurzen, URL-sicheren String und leg ihn in einen Query-Parameter wie ?filter=. In Wingo UI schreibt serializeFilter(filter) aus lib/filters den String, und parseFilter(text) liest ihn wieder ein. Bei einem veralteten oder von Hand bearbeiteten Link gibt es null zurück, und die Seite zeigt einfach die ungefilterte Liste.

### Sollen Filter sofort greifen oder erst mit einem Apply-Button?

Lass sie sofort greifen, wenn Filtern billig ist, etwa bei einer Liste, die schon im Browser liegt. Dann sieht man die Wirkung jeder Änderung direkt. Nimm einen Apply-Button, wenn jede Änderung einen Server-Request oder eine langsame Abfrage kostet, und arbeite auf Smartphones immer mit einem Entwurf, weil der Filter-Editor dort die Liste verdeckt.

### Wie filtere ich eine React-Tabelle mit AND / OR-Bedingungen auf dem Server?

Schick den Filterbaum an einen Endpoint, lauf rekursiv durch ihn und mach aus jeder Bedingung eine parametrisierte SQL-Klausel, verbunden mit AND oder OR, Gruppen in Klammern. Bilde Feld-IDs über eine Whitelist auf Spalten ab, damit ein Client nie einen Spaltennamen einschleusen oder einen Operator schicken kann, den du nicht unterstützt.

---

Quelle: https://wingo-ui.com/de/blog/react-filter-builder
