Filter Builder
Field, operator and value filter chips with a matchesFilter helper for your rows.
@drift-ui/filter-builderRegistry JSON- MY-104Invoice totals round twice3 pts
- MY-112Dark mode chart contrast2 pts
- MY-118Invoice PDF missing logo1 pts
- MY-121Retry webhook deliveries5 pts
On this page
Installation
The CLI also installs the npm packages and registry items it builds on. Register the namespace once first with https://drift-ui.dev/r/{style}/{name}.json. The CLI fills in {style} from your components.json and installs the Base UI or Radix build to match. See setup.
API reference
FilterBuilder
A row of filter chips with an Add filter menu. Each chip edits its operator and value in place.
| Prop | Type | Default |
|---|---|---|
fieldsRequiredFilterField[]Filterable fields: { id, label, type: "text" | "number" | "select", options?, icon? }. | FilterField[] | |
valueFilter[]Controlled filters. | Filter[] | |
defaultValueFilter[]= []Initial value when uncontrolled. | Filter[] | [] |
onValueChange(filters: Filter[]) => voidCalled when a filter is added, changed or removed. | (filters: Filter[]) => void | |
aria-labelstring= "Filters"Name of the filter group. | string | "Filters" |
Other props go to the <div>.
matchesFilter
matchesFilter(fieldValue: string | number, filter: Filter): booleanImport from @/lib/filter to apply a Filter ({ id, fieldId, operator, value }) to your rows. An empty filter value matches everything. Text fields use contains, not-contains, is and is-not, numbers use eq, gt and lt, and selects use is and is-not.
Source
"use client"
import * as React from "react"
import { ListFilterIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import {
createFilter,
type Filter,
type FilterField,
} from "@/lib/filter"
import { useControllableState } from "@/hooks/use-controllable-state"
import { Button } from "@/components/ui/button"
import { FilterChip } from "@/components/ui/filter-chip"
import { FilterMenu, FilterMenuItem } from "@/components/ui/filter-menu"
function FilterBuilder({
fields,
value,
defaultValue = [],
onValueChange,
"aria-label": ariaLabel = "Filters",
className,
...props
}: Omit<React.ComponentProps<"div">, "defaultValue" | "onChange"> & {
fields: FilterField[]
value?: Filter[]
defaultValue?: Filter[]
onValueChange?: (filters: Filter[]) => void
}) {
const { value: filters, setValue: setFilters } = useControllableState({
value,
defaultValue,
onChange: onValueChange,
})
const [newFilterId, setNewFilterId] = React.useState<string | null>(null)
const didAddFilterRef = React.useRef(false)
const fieldsById = new Map(fields.map((field) => [field.id, field]))
function addFilter(field: FilterField) {
const filter = createFilter(field)
setFilters((current) => [...current, filter])
setNewFilterId(filter.id)
didAddFilterRef.current = true
}
function updateFilter(next: Filter) {
setFilters((current) =>
current.map((filter) => (filter.id === next.id ? next : filter))
)
}
function removeFilter(id: string) {
setFilters((current) => current.filter((filter) => filter.id !== id))
}
return (
<div
role="group"
aria-label={ariaLabel}
data-slot="filter-builder"
className={cn("flex flex-wrap items-center gap-2", className)}
{...props}
>
{filters.map((filter) => {
const field = fieldsById.get(filter.fieldId)
if (!field) return null
return (
<FilterChip
key={filter.id}
field={field}
filter={filter}
isNew={filter.id === newFilterId}
onFilterChange={updateFilter}
onRemove={() => removeFilter(filter.id)}
/>
)
})}
<FilterMenu
onCloseAutoFocus={(event) => {
if (!didAddFilterRef.current) return
didAddFilterRef.current = false
event.preventDefault()
}}
trigger={
<Button
variant="outline"
size="sm"
data-slot="filter-builder-add"
aria-label="Add filter"
className="border-dashed"
>
<ListFilterIcon />
{filters.length === 0 && "Filter"}
</Button>
}
>
{fields.map((field) => (
<FilterMenuItem key={field.id} onSelect={() => addFilter(field)}>
{field.icon}
{field.label}
</FilterMenuItem>
))}
</FilterMenu>
{filters.length > 0 && (
<Button
variant="ghost"
size="sm"
data-slot="filter-builder-clear"
onClick={() => setFilters([])}
>
Clear
</Button>
)}
</div>
)
}
export { FilterBuilder }