# Filter Bar

> Add, view, edit, and clear filters above any collection in one consistent row.

Source: https://www.honestui.com/docs/product/filter-bar

```tsx
"use client"

import * as React from "react"

import {
  FilterBar,
  type FilterValue,
} from "@/registry/default/product/filter-bar/filter-bar"
import {
  CREATED_PRESETS,
  CATEGORY_OPTIONS,
  STATUS_OPTIONS,
} from "./filter-bar-example-data"

/**
 * The default HonestUI setup: Status with counts, a searchable Category
 * list, a Created range, and an Amount field. Results respond on every
 * selection because mode stays instant.
 */
export function FilterBarDemo() {
  const [value, setValue] = React.useState<FilterValue[]>([
    { key: "status", operator: "is", value: ["active", "pending"] },
    { key: "category", operator: "is", value: ["design"] },
    {
      key: "created",
      operator: "between",
      value: CREATED_PRESETS[1].value(),
    },
  ])

  return (
    <div className="w-full min-w-0">
      <FilterBar
        filters={[
          {
            key: "status",
            label: "Status",
            type: "multi-select",
            options: STATUS_OPTIONS,
          },
          {
            key: "category",
            label: "Category",
            type: "multi-select",
            searchable: true,
            options: CATEGORY_OPTIONS,
          },
          {
            key: "created",
            label: "Created",
            type: "date-range",
            meta: { datePresets: CREATED_PRESETS },
          },
          { key: "amount", label: "Amount", type: "number", meta: { prefix: "$" } },
        ]}
        value={value}
        onValueChange={setValue}
      />
      <p aria-live="polite" className="mt-3 text-sm text-muted-foreground">
        Filtering orders locally. Open the panel or edit a chip; every change
        commits immediately.
      </p>
    </div>
  )
}

```

## Overview [#overview]

Filter Bar answers a question that shows up on nearly every admin screen: how does someone narrow this list? Status pickers, category checkboxes, price ranges, date windows, customer searches. The component gives all of them one place to live, one shape for their values, and one way to disappear when nobody is using them.

The bar stays visually quiet until filters are active. With nothing applied you see a single Filter button. Once someone picks three filters the button gains a count, chips appear beside it naming exactly what is narrowing the view, and a Clear all action shows up at the end. Nothing renders just to fill space: no zero badge, no empty chips, no dead Clear action.

Filter Bar owns filter presentation and filter values. It never filters rows itself, never calls your API, and never builds a query string. You pass an array of definitions plus the current value, and you receive changes back through one callback. That boundary is why it sits as comfortably over local TanStack Table state as over a server request. [Data Table](/docs/product/data-table) stays responsible for its own rows; see the integration example below.

When one small facet is enough, a menu or a handful of checkboxes already solves it. Reach for Filter Bar once several filter types need to coexist, which is where hand-built toolbars usually start disagreeing with themselves.

## Anatomy [#anatomy]

<Anatomy title="FilterBar">
  <AnatomyItem name="Trigger" description="Filter button, active count badge, rotating chevron" />

  <AnatomyItem name="Values" description="One removable chip per active filter, collapsing to +N" />

  <AnatomyItem name="Clear all" description="Quiet text action for committed filters" />

  <AnatomyItem name="Panel" description="Header, groups grid, footer inside a Popover or Sheet">
    <AnatomyItem name="Group" description="Label, optional selection count, one filter control" />

    <AnatomyItem name="Footer" description="Cancel and Apply filters in apply mode" />
  </AnatomyItem>
</Anatomy>

Installing the root component covers everything above. Parts export separately too: `FilterBarToolbar`, `FilterBarTrigger`, `FilterBarValues`, `FilterBarValue`, `FilterBarClear`, `FilterBarContent`, `FilterBarHeader`, `FilterBarTitle`, `FilterBarGroups`, `FilterBarGroup`, `FilterBarGroupLabel`, `FilterBarFooter`, `FilterBarApply`, and `FilterBarCancel`. The data-driven API runs these same parts internally, so composed layouts get identical behavior rather than a second implementation.

Each filter field is built on an HonestUI control: `Input` for text, `Select` for short known choices, checkbox rows for multi-select, `NumberField` for amounts, `RadioGroup` for preset dates and booleans, `Switch` where one toggle fits better than three states, and `Field` semantics connecting labels to controls. Custom ranges reuse the shared [Date Range Picker](/docs/product/date-range-picker) instead of growing a second calendar here.

## Behavior [#behavior]

### Modes [#modes]

`mode="instant"` commits on every change. Local filtering, cheap queries, anything where users benefit from watching results move right away. The panel simply ends after the groups; there is nothing to confirm.

`mode="apply"` holds edits in a draft while the panel is open. Apply filters commits the draft and closes. Cancel discards it. Escape and clicking outside discard too, because silently applying unfinished work behind someone's back is worse than losing a click. The header shows Clear all filters only while a draft exists, and it empties the draft without closing so users can rebuild from scratch. This is deliberately different from the Clear all in the bar, which clears committed filters immediately even mid-draft.

### Counts [#counts]

Two different numbers matter, and the component keeps them apart. The trigger badge counts active filter fields: Status with Active and Pending selected counts as one. It would be confusing if adding a second value inside a multi-select inflated the count. Group headings show the opposite number: selections inside that group.

### Chips [#chips]

Chips print what humans asked for, never internal shapes: `Status: Active, Pending`, `Amount: $100 to $500`, `Created: Last 30 days`. Values that match a configured date preset show the preset name; others fall back to compact dates like Aug 1 to Aug 26. A definition can pass `formatValue` for anything the defaults phrase awkwardly.

Long values truncate inside a capped chip width, and the full text lives in the accessible name plus the native tooltip, so nothing important hides. When the row runs out of room, collapse mode trims the tail into one `+N` control that reopens the panel. Set `valuesDisplay="wrap"` when your layout has room for every chip.

Clicking a chip body reopens the panel scrolled to that group. The cross removes the filter immediately.

### Options, search, and loading [#options-search-and-loading]

Multi-select lists grow a search input past `searchableThreshold` entries (ten by default), and `searchable: true` forces it either way. Search is case-insensitive and only changes visibility; selected values stay selected and stay listed even when filtered out of view.

Pass `loadOptions` for server-backed choices. Requests debounce by 250 milliseconds, previous results remain visible while a newer query loads, and empty responses show No results match instead of a blank list. When a request fails the field says so, keeps whatever was committed, and offers Try again; unrelated filters keep working.

Result counts next to option labels are display data you supply, rendered muted with tabular numerals. `showOptionCounts` turns rendering off globally without touching definitions. Filter Bar never calculates facets itself.

### Dependent and disabled filters [#dependent-and-disabled-filters]

Country depending on Region is application logic: update State's options in your state handler and drop its entry when a new Country invalidates the old value. Keep the disabled filter visible with a `disabledReason`; hiding it makes the jump in the layout and leaves the dependency undiscoverable.

## Accessibility [#accessibility]

The trigger announces as Filter, followed by N active filters whenever the count is positive. Panels carry the heading Filters. Every group has a visible label connected to its controls, remove actions say Remove Status filter rather than a bare Remove, and counts are spoken text rather than decoration.

Keyboard work mirrors the mouse:

| Key            | Action                                             |
| -------------- | -------------------------------------------------- |
| Enter or Space | Open the panel                                     |
| Tab            | Move across trigger, chips, inputs, footer actions |
| Space          | Toggle a focused checkbox or radio choice          |
| Escape         | Close the panel, discarding drafts in apply mode   |

Closing always returns focus to the trigger. Chip removal buttons are individually focusable, the +N summary is a real button, and clearing is reachable by keyboard wherever it appears. Color never carries state alone: selection pairs fills with checked states, badges pair number with position, and popover motion honors reduced-motion settings.

## Installation [#installation]


  

  
    <CliBlock commands="[&#x22;filter-bar&#x22;, &#x22;date-range-picker&#x22;]" />
  

  
    
      
        Install the calendar and date utility packages used by range filters:
      

      ```bash
      npm install react-day-picker date-fns
      ```

      
        Copy the Date Range Picker component that custom ranges build on.
      

      <ComponentSource name="date-range-picker" title="components/ui/date-range-picker/date-range-picker.tsx" file="date-range-picker.tsx" />

      <ComponentSource name="date-range-picker" title="components/ui/date-range-picker/date-range-calendar.tsx" file="date-range-calendar.tsx" />

      <ComponentSource name="date-range-picker" title="components/ui/date-range-picker/date-range-presets.tsx" file="date-range-presets.tsx" />

      <ComponentSource name="date-range-picker" title="components/ui/date-range-picker/date-range-utils.ts" file="date-range-utils.ts" />

      
        Copy and paste the Filter Bar source.
      

      <ComponentSource name="filter-bar" title="components/ui/filter-bar/filter-bar-types.ts" file="filter-bar-types.ts" />

      <ComponentSource name="filter-bar" title="components/ui/filter-bar/filter-bar-utils.ts" file="filter-bar-utils.ts" />

      <ComponentSource name="filter-bar" title="components/ui/filter-bar/filter-bar-context.tsx" file="filter-bar-context.tsx" />

      <ComponentSource name="filter-bar" title="components/ui/filter-bar/filter-bar-fields.tsx" file="filter-bar-fields.tsx" />

      <ComponentSource name="filter-bar" title="components/ui/filter-bar/filter-bar.tsx" file="filter-bar.tsx" />

      
        Update the import paths to match your project setup.
      
    
  


## Usage [#usage]

Controlled state is the main documented path, and honestly the one you want anyway, because filtering usually touches something else nearby:

```tsx
import { FilterBar } from "@/components/ui/filter-bar/filter-bar"

const filters = [
  {
    key: "status",
    label: "Status",
    type: "multi-select",
    options: [
      { label: "Active", value: "active", count: 124 },
      { label: "Pending", value: "pending", count: 32 },
      { label: "Archived", value: "archived", count: 18 },
    ],
  },
]

const [value, setValue] = useState<FilterValue[]>([])

<FilterBar filters={filters} value={value} onValueChange={setValue} />
```

The value is one consistent structure regardless of filter type:

```ts
type FilterValue = {
  key: string
  operator?: string
  value: unknown
}
```

It is UI state, not a database schema. Transform it in `onValueChange` before it reaches a URL, a fetch body, or TanStack's column filters:

```tsx
onValueChange={(next) => {
  setFilters(next)
  updateSearchParams(toUrlParams(next))
}}
```

Uncontrolled use works when a prototype just needs a working toolbar: pass `defaultValue` and read values back later through `onValueChange`.

### Defaults worth knowing [#defaults-worth-knowing]

Instant mode, collapsed values, search past ten options, counts shown when provided, Sheet below 640px wide, panel aligned start. All are props; none require configuration.

## Don't do this [#dont-do-this]

### Counting selected values as filters [#counting-selected-values-as-filters]

```tsx
// Bad. One multi-select suddenly outweighs three whole filters.
const count = filters.reduce((sum, f) => sum + (f.value.length ?? 1), 0)
```

Count active keys instead. Filter Bar does this for the trigger badge already, and group headings report per-group selections where that detail belongs.

### Reading outside clicks as approval [#reading-outside-clicks-as-approval]

```tsx
// Bad. Users who changed their mind still commit expensive queries.
onOpenChange={(open) => {
  if (!open && mode === "apply") applyDraft()
}}
```

Outside clicks behave like Cancel. If the query behind these filters is cheap enough that accidental application hurts less than re-opening the panel, use instant mode; it exists for exactly that tradeoff.

## Examples [#examples]

### Searchable long lists [#searchable-long-lists]

Fourteen categories with facet counts. Type finance to watch non-matches leave, selections included, and try zebra for the empty message.

```tsx
"use client"

import * as React from "react"

import {
  FilterBar,
  type FilterValue,
} from "@/registry/default/product/filter-bar/filter-bar"
import { CATEGORY_OPTIONS } from "./filter-bar-example-data"

/** A long list where typing pays off: search stays on, selections survive. */
export function FilterBarSearchable() {
  const [value, setValue] = React.useState<FilterValue[]>([])

  return (
    <div className="w-full min-w-0">
      <FilterBar
        filters={[
          {
            key: "category",
            label: "Category",
            type: "multi-select",
            searchable: true,
            options: CATEGORY_OPTIONS,
          },
        ]}
        value={value}
        onValueChange={setValue}
      />
      <p className="mt-3 text-sm text-muted-foreground">
        Search ignores case and never clears what you already picked. Try a
        word with no matches to see the empty state.
      </p>
    </div>
  )
}

```

### Number rules [#number-rules]

A price filter starting between $100 and $500. Switch the rule and notice the wording change on both input pairing and chip text.

```tsx
"use client"

import * as React from "react"

import {
  FilterBar,
  type FilterValue,
} from "@/registry/default/product/filter-bar/filter-bar"

/** Numeric comparisons: the second value appears only for Between. */
export function FilterBarNumberRange() {
  const [value, setValue] = React.useState<FilterValue[]>([
    { key: "price", operator: "between", value: { min: 100, max: 500 } },
  ])

  return (
    <div className="w-full min-w-0">
      <FilterBar
        filters={[
          {
            key: "price",
            label: "Price",
            type: "number",
            meta: { prefix: "$", min: 0, step: 1 },
          },
        ]}
        value={value}
        onValueChange={setValue}
      />
      <p className="mt-3 text-sm text-muted-foreground">
        The chip reads Price: $100 to $500. Switch the rule to Over or Under
        and the wording follows.
      </p>
    </div>
  )
}

```

### Preset dates plus custom windows [#preset-dates-plus-custom-windows]

Committed ranges match Last 7 days or Last 30 days and shorten into a named chip. Pick Custom to reach the shared Date Range Picker.

```tsx
"use client"

import * as React from "react"

import {
  FilterBar,
  type FilterValue,
} from "@/registry/default/product/filter-bar/filter-bar"
import { CREATED_PRESETS } from "./filter-bar-example-data"

/**
 * Presets matched against the stored range keep the chip short; a custom
 * pick opens the Date Range Picker calendar instead.
 */
export function FilterBarDateRange() {
  const [value, setValue] = React.useState<FilterValue[]>([
    { key: "created", operator: "between", value: CREATED_PRESETS[0].value() },
  ])

  return (
    <div className="w-full min-w-0">
      <FilterBar
        filters={[
          {
            key: "created",
            label: "Created",
            type: "date-range",
            meta: { datePresets: CREATED_PRESETS },
          },
        ]}
        value={value}
        onValueChange={setValue}
      />
      <p className="mt-3 text-sm text-muted-foreground">
        Committed ranges that match a preset show its name. Anything else
        falls back to two compact dates.
      </p>
    </div>
  )
}

```

### Text operators [#text-operators]

Contains starts prefilled. Choose Is empty to watch the input disappear entirely instead of lingering beneath a rule that ignores it.

```tsx
"use client"

import * as React from "react"

import {
  FilterBar,
  type FilterValue,
} from "@/registry/default/product/filter-bar/filter-bar"

/** Text rules. Choosing Is empty removes the input, as it should. */
export function FilterBarText() {
  const [value, setValue] = React.useState<FilterValue[]>([
    { key: "name", operator: "contains", value: "Acme" },
  ])

  return (
    <div className="w-full min-w-0">
      <FilterBar
        filters={[
          {
            key: "name",
            label: "Customer",
            type: "text",
            meta: { placeholder: "Search value..." },
          },
        ]}
        value={value}
        onValueChange={setValue}
      />
      <p className="mt-3 text-sm text-muted-foreground">
        Contains, equals, prefixes, and the two empty rules cover almost every
        text filter a list needs.
      </p>
    </div>
  )
}

```

### Apply before commit [#apply-before-commit]

Change several things, close the panel carelessly, and check the readout underneath: nothing reached the application state until Apply filters ran.

```tsx
"use client"

import * as React from "react"

import {
  FilterBar,
  type FilterValue,
} from "@/registry/default/product/filter-bar/filter-bar"
import { STATUS_OPTIONS } from "./filter-bar-example-data"

function describe(value: FilterValue[]) {
  if (value.length === 0) return "no filters"

  return value
    .map((entry) => {
      if (Array.isArray(entry.value)) return `${entry.key}: ${entry.value.join(", ")}`

      return `${entry.key}: ${String(entry.value)}`
    })
    .join(" | ")
}

/**
 * Apply mode keeps a draft behind the panel. Cancel, Escape, and clicking
 * outside all discard; only Apply filters commits, which is exactly what
 * expensive queries want.
 */
export function FilterBarApply() {
  const [value, setValue] = React.useState<FilterValue[]>([])
  const [lastCommitted, setLastCommitted] = React.useState("")

  return (
    <div className="w-full min-w-0">
      <FilterBar
        filters={[
          {
            key: "status",
            label: "Status",
            type: "multi-select",
            options: STATUS_OPTIONS,
          },
          {
            key: "archived",
            label: "Archived",
            type: "boolean",
            meta: { booleanStyle: "radio" },
          },
        ]}
        mode="apply"
        value={value}
        onValueChange={(next) => {
          setValue(next)
          setLastCommitted(describe(next))
        }}
      />
      <p aria-live="polite" className="mt-3 text-sm text-muted-foreground">
        Committed: {lastCommitted || describe(value)}. Close without Apply to
        watch the draft disappear.
      </p>
    </div>
  )
}

```

### Server-loaded options [#server-loaded-options]

Customers load after a delay, debounced while you type. Flip the failure switch to force an error state, then use Try again; whatever you had selected survives the whole episode.

```tsx
"use client"

import * as React from "react"

import { Switch } from "@/components/honest-ui/ui/switch"
import {
  FilterBar,
  type FilterOption,
  type FilterValue,
} from "@/registry/default/product/filter-bar/filter-bar"
import { CUSTOMER_DIRECTORY } from "./filter-bar-example-data"

const REQUEST_LATENCY_MS = 650

/**
 * Options load like they would from an API. Flip the switch to make requests
 * fail on purpose: existing selections stay, the field explains itself, and
 * Try again recovers without touching other filters.
 */
export function FilterBarAsync() {
  const [value, setValue] = React.useState<FilterValue[]>([])
  const [failRequests, setFailRequests] = React.useState(false)
  const attemptsRef = React.useRef(0)

  async function loadCustomers(query: string): Promise<FilterOption[]> {
    const attempt = ++attemptsRef.current

    await new Promise((resolve) => setTimeout(resolve, REQUEST_LATENCY_MS))

    if (failRequests && attempt % 2 === 1) {
      throw new Error("request failed")
    }

    const needle = query.trim().toLowerCase()

    return CUSTOMER_DIRECTORY.filter((name) =>
      name.toLowerCase().includes(needle)
    ).slice(0, 12).map((label) => ({ label, value: label }))
  }

  return (
    <div className="w-full min-w-0">
      <FilterBar
        filters={[
          {
            key: "customer",
            label: "Customer",
            type: "multi-select",
            searchable: true,
            loadOptions: (query) => loadCustomers(query),
          },
        ]}
        value={value}
        onValueChange={setValue}
      />
      <p className="mt-4 flex items-center gap-2 text-sm text-muted-foreground">
        <Switch
          checked={failRequests}
          onCheckedChange={(checked) => setFailRequests(checked === true)}
          aria-label="Fail every other request"
        />
        Fail requests to see the error state and recovery.
      </p>
    </div>
  )
}

```

### A filter we did not predict [#a-filter-we-did-not-predict]

Distance ships as a custom renderer over a plain slider. Its formatValue keeps the chip readable, proving custom controls connect to the rest of the system rather than floating beside it.

```tsx
"use client"

import * as React from "react"

import { Slider } from "@/components/honest-ui/ui/slider"
import {
  FilterBar,
  type FilterRenderProps,
  type FilterValue,
} from "@/registry/default/product/filter-bar/filter-bar"

function DistanceControl({
  value,
  onChange,
  disabled,
  labelId,
  descriptionId,
}: FilterRenderProps) {
  const range = Array.isArray(value) ? (value as number[]) : [0, 50]

  return (
    <div className="flex flex-col gap-3 pb-1">
      <Slider
        aria-labelledby={labelId}
        aria-describedby={descriptionId}
        value={range}
        min={0}
        max={50}
        step={1}
        onValueChange={(next) => onChange(next)}
        disabled={disabled}
      />
      <p className="text-xs text-muted-foreground tabular-nums">
        Delivering within {range[0]} to {range[1]} km
      </p>
    </div>
  )
}

/**
 * Anything the built-in types miss gets a render function. The custom control
 * still writes through Filter Bar, so chips and Clear all keep working.
 */
export function FilterBarCustom() {
  const [value, setValue] = React.useState<FilterValue[]>([])

  return (
    <div className="w-full min-w-0">
      <FilterBar
        filters={[
          {
            key: "distance",
            label: "Distance",
            type: "custom",
            formatValue: (raw) => {
              if (!Array.isArray(raw)) return ""

              const [from, to] = raw as number[]

              if (from === 0 && to === 50) return ""

              return `${from} to ${to} km`
            },
            render: DistanceControl,
          },
        ]}
        value={value}
        onValueChange={setValue}
      />
      <p className="mt-3 text-sm text-muted-foreground">
        The renderer receives value, onChange, clear, disabled, label and
        description IDs, and the definition; it stays connected to everything
        else.
      </p>
    </div>
  )
}

```

### External ownership [#external-ownership]

State lives in the parent, Reset proves resets cost one call, and every commit increments a counter you could imagine feeding analytics or URL sync.

```tsx
"use client"

import * as React from "react"

import { Button } from "@/components/honest-ui/ui/button"
import {
  FilterBar,
  type FilterValue,
} from "@/registry/default/product/filter-bar/filter-bar"
import { STATUS_OPTIONS } from "./filter-bar-example-data"

/**
 * The application owns the array end to end. Reset proves it: one call clears
 * what the panel shows, chips, and the trigger count.
 */
export function FilterBarControlled() {
  const [value, setValue] = React.useState<FilterValue[]>([
    { key: "status", operator: "is", value: ["active"] },
  ])
  const changeCountRef = React.useRef(0)
  const [commitNote, setCommitNote] = React.useState("")

  return (
    <div className="w-full min-w-0">
      <div className="flex items-center gap-3">
        <FilterBar
          filters={[
            {
              key: "status",
              label: "Status",
              type: "multi-select",
              options: STATUS_OPTIONS,
            },
          ]}
          value={value}
          onValueChange={(next) => {
            changeCountRef.current += 1
            setCommitNote(`Commit #${changeCountRef.current}`)
            setValue(next)
          }}
        />
        <Button
          variant="ghost"
          size="sm"
          onClick={() => setValue([])}
          className="shrink-0"
        >
          Reset
        </Button>
      </div>
      <p aria-live="polite" className="mt-3 text-sm text-muted-foreground">
        External state owns every value{commitNote ? `; ${commitNote} received` : ""}.
      </p>
    </div>
  )
}

```

### Above a Data Table [#above-a-data-table]

Six lines map filter values onto sample orders; nothing about Filter Bar knows Data Table exists. The same callback shape drives Data Grid columns, server params, or whatever else your collection needs.

```tsx
"use client"

import * as React from "react"

import {
  DataTable,
  type DataTableProps,
} from "@/registry/default/product/data-table/data-table"
import {
  FilterBar,
  type FilterValue,
} from "@/registry/default/product/filter-bar/filter-bar"
import { SAMPLE_ORDERS, STATUS_OPTIONS } from "./filter-bar-example-data"

type Order = (typeof SAMPLE_ORDERS)[number]

const columns: DataTableProps<Order>["columns"] = [
  { accessorKey: "customer", header: "Customer" },
  { accessorKey: "status", header: "Status" },
  { accessorKey: "category", header: "Category" },
  {
    accessorKey: "amount",
    header: "Amount",
    meta: { align: "right" as const, label: "Amount" },
    cell: ({ row }) =>
      new Intl.NumberFormat("en-US", {
        style: "currency",
        currency: "USD",
        maximumFractionDigits: 0,
      }).format(row.original.amount),
  },
]

function rowMatches(order: Order, filters: FilterValue[]) {
  return filters.every((filter) => {
    if (filter.key === "status" && Array.isArray(filter.value)) {
      return filter.value.includes(order.status.toLowerCase())
    }

    if (filter.key === "customer" && typeof filter.value === "string") {
      return order.customer.toLowerCase().includes(filter.value.toLowerCase())
    }

    if (filter.key === "amount") {
      if (typeof filter.value === "number") {
        return order.amount === filter.value
      }

      if (typeof filter.value === "object" && filter.value != null) {
        const { min, max } = filter.value as { min?: number; max?: number }

        if (min != null && order.amount < min) return false
        if (max != null && order.amount > max) return false

        return true
      }
    }

    // Unhandled keys are the application's problem; keep rows visible.
    return true
  })
}

/**
 * Filter Bar stays generic: values in, callbacks out. Mapping those values to
 * a Data Table's rows takes six lines here and zero Filter Bar changes.
 */
export function FilterBarDataTable() {
  const [filters, setFilters] = React.useState<FilterValue[]>([])

  const rows = React.useMemo(
    () => SAMPLE_ORDERS.filter((order) => rowMatches(order, filters)),
    [filters]
  )

  return (
    <div className="flex w-full min-w-0 flex-col gap-4">
      <FilterBar
        filters={[
          {
            key: "status",
            label: "Status",
            type: "multi-select",
            options: STATUS_OPTIONS,
          },
          {
            key: "customer",
            label: "Customer",
            type: "text",
            meta: { placeholder: "Search customer..." },
          },
          { key: "amount", label: "Amount", type: "number", meta: { prefix: "$" } },
        ]}
        value={filters}
        onValueChange={setFilters}
      />
      <p aria-live="polite" className="text-sm text-muted-foreground">
        {rows.length} of {SAMPLE_ORDERS.length} orders
      </p>
      <DataTable columns={columns} data={rows} caption="Orders" />
    </div>
  )
}

```

## API reference [#api-reference]

### FilterBar [#filterbar]

| Prop                                    | Type                              | Default          | Description                                                |
| --------------------------------------- | --------------------------------- | ---------------- | ---------------------------------------------------------- |
| `filters`                               | `FilterDefinition[]`              | None             | Available filters, ordered by `priority` then declaration. |
| `value`                                 | `FilterValue[]`                   | None             | Controlled committed value.                                |
| `defaultValue`                          | `FilterValue[]`                   | `[]`             | Initial uncontrolled value.                                |
| `onValueChange`                         | `(values: FilterValue[]) => void` | None             | Receives committed values, including clears.               |
| `mode`                                  | `"instant" \| "apply"`            | `"instant"`      | Draft handling described under Behavior.                   |
| `open` / `defaultOpen` / `onOpenChange` | boolean, boolean, callback        | Uncontrolled     | Panel open state.                                          |
| `valuesDisplay`                         | `"collapse" \| "wrap"`            | `"collapse"`     | Overflow handling for chips.                               |
| `searchableThreshold`                   | number                            | `10`             | Option count that adds search automatically.               |
| `showOptionCounts`                      | boolean                           | `true`           | Renders supplied `count` data on options.                  |
| `mobileBreakpoint`                      | number                            | `640`            | Width below which the panel becomes a Sheet.               |
| `popoverAlign`                          | `"start" \| "center" \| "end"`    | `"start"`        | Desktop panel alignment relative to the trigger.           |
| `disabled`                              | boolean                           | `false`          | Disables the trigger and all writes.                       |
| `labels`                                | partial labels object             | English defaults | Overrides every string listed below.                       |

Any other div prop forwards to the root element.

### FilterDefinition [#filterdefinition]

| Prop                         | Applies to                 | Description                                                                                                                                                               |
| ---------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `key`, `label`               | all                        | Identity plus chip and group text.                                                                                                                                        |
| `type`                       | all                        | `text`, `select`, `multi-select`, `number`, `date`, `date-range`, `boolean`, or `custom`.                                                                                 |
| `options`                    | select types               | `{ label, value, count?, disabled? }` entries. Use string or number values.                                                                                               |
| `operators`                  | text, select, number, date | Restricts or reorders the default operator set.                                                                                                                           |
| `defaultOperator`            | those types                | Picks which rule reads first.                                                                                                                                             |
| `searchable`                 | multi-select               | Forces search beyond the threshold decision.                                                                                                                              |
| `loadOptions(query)`         | multi-select, select       | Async resolver returning options; debounce happens for you.                                                                                                               |
| `priority`                   | all                        | Lower numbers appear earlier.                                                                                                                                             |
| `placeholder`                | most                       | Empty-state hint forwarded to the control.                                                                                                                                |
| `disabled`, `disabledReason` | all                        | Keep unavailable filters visible with their explanation.                                                                                                                  |
| `meta`                       | varies                     | `prefix`, `suffix`, `min`, `max`, `step` for numbers; `datePresets` for dates; `booleanStyle: "radio" \| "switch"` for booleans.                                          |
| `formatValue(value)`         | all                        | Replaces automatic chip phrasing.                                                                                                                                         |
| `render(props)`              | `custom`                   | Full control: receives `value`, `operator`, `onChange`, `clear`, `disabled`, `labelId`, `descriptionId`, and `definition`. Connect the custom widget to the supplied IDs. |

Operators ship per type, sensible defaults first:

* text: Contains, Does not contain, Is, Is not, Starts with, Ends with, Is empty, Is not empty
* number: Equals, Does not equal, Greater than, Greater than or equal, Less than, Less than or equal, Between, Is empty, Is not empty
* date: On, Before, After
* select and boolean: Is

Selection filters omit the operator picker when only one reading exists, which is nearly always the right call for Status.

### Labels [#labels]

| Key                               | Default                       |
| --------------------------------- | ----------------------------- |
| `trigger`                         | Filter                        |
| `triggerActiveSuffix(count)`      | N active filter(s)            |
| `title`                           | Filters                       |
| `clearAll`                        | Clear all                     |
| `clearPanel`                      | Clear all filters             |
| `cancel` / `apply`                | Cancel / Apply filters        |
| `moreCount(n)`                    | +N                            |
| `removeFilter(label)`             | Remove X filter               |
| `loadingOptions` / `retryOptions` | Loading... / Try again        |
| `optionsError`                    | Could not load these options. |
| `noMatches(query)`                | No results match "...".       |

Components exported alongside: `FilterBarToolbar`, `FilterBarTrigger`, `FilterBarValues`, `FilterBarValue`, `FilterBarClear`, `FilterBarContent`, `FilterBarHeader`, `FilterBarTitle`, `FilterBarGroups`, `FilterBarGroup`, `FilterBarGroupLabel`, `FilterBarFooter`, `FilterBarApply`, `FilterBarCancel`, and `FilterBarChip`.
