Search documentation

Search for a page or heading...

tablecn
0

Filter components

The Filter button, panel and chips, their props, and how to adapt them.

The filter-builder block is the filter UI in the table's toolbar. The CLI copies it into components/filter/, or the folder you pass to --path (see Installation), so every file is yours to edit. The Radix UI, Base UI and React Aria versions have the same files and props.

All components must be rendered inside a FilterProvider (or NextFilterProvider).

Usage

Usage
import { FilterBuilder } from "@/components/filter/filter-builder"
import { FilterChips } from "@/components/filter/filter-chips"

<div className="flex flex-wrap items-center gap-2">
  <FilterBuilder />
  <FilterChips />
</div>
  • FilterBuilder is a Filter button showing how many filters are applied. It opens the panel in a popover. Opening an empty filter starts with one blank rule, and closing without applying drops the edits.
  • FilterChips shows the applied rules ("Status is Paid"). Each chip's × removes that rule right away.

FilterBuilder

Prop

Type

FilterBuilderPanel

The panel on its own: rule rows, AND/OR, Add filter, Clear all and Apply. Clear all applies at once: it doesn't wait for Apply. Use it where a popover doesn't fit, such as a sidebar or a sheet:

Sidebar panel
import { FilterBuilderPanel } from "@/components/filter/filter-builder-panel"

<aside className="w-[28rem] border-l p-4">
  <FilterBuilderPanel onApply={() => setSheetOpen(false)} />
</aside>

Prop

Type

The panel is a <form>, and Apply is its submit button:

  • Enter in a text field applies.
  • Every other button is type="button", so nothing else submits.
  • Submitting never reaches a form around the component (React propagates events through portals, so the popover would otherwise submit your page's form).

Don't render the standalone panel inside your own <form>: nested forms are invalid HTML. FilterBuilder is fine there, since its panel renders in a portal.

When the panel is used on its own, the chips' × applies at once, which also drops unapplied edits in the panel.

FilterChips

Prop

Type

Renders nothing when no filter is applied. Values are shown the way users entered them: option labels (resolved with resolveLabels if needed), Yes/No, dates in the user's locale. After a chip is removed, focus moves to the next chip, or the previous one for the last. Removing the only chip removes the list, so focus falls back to the page; move it to your Filter button if you prefer.

Keyboard and accessibility

  • The trigger announces the count ("2 filters") to screen readers; the badge is decorative.
  • After Add filter, focus goes to the new row. After removing a row or clearing all, it goes to Add filter.
  • Picking an operator moves focus to the value input.
  • Warning icons open on press rather than hover, so they work on touch screens. Each has an accessible label with the warning text.
  • Buttons have labels from messages, e.g. "Remove filter: Status is Paid".

React Aria's popover is modal: clicking outside, for example on a chip's ×, only closes the panel. With Radix UI and Base UI, the click also goes through.

Files

FileExportsRole
filter-builder.tsxFilterBuilderTrigger button + popover
filter-builder-panel.tsxFilterBuilderPanelRules, join, Add / Clear all / Apply
filter-chips.tsxFilterChipsApplied rules as removable chips
filter-rule-row.tsxFilterRuleRow, FilterValueSlotPropsOne rule: field, operator, value, warnings, remove
filter-field-select.tsxFilterFieldSelectSearchable field picker
filter-operator-select.tsxFilterOperatorSelectOperator picker
filter-join-select.tsxFilterJoinSelectAND / OR
filter-value-input.tsxFilterValueInput, filterValueInputsPicks the value input by field type
filter-text-value-input.tsxTextValueInput, NumberValueInputText inputs, single or range
filter-boolean-value-input.tsxBooleanValueInputYes / No
filter-select-value-input.tsxSelectValueInputOptions, single or multiple, static or async
filter-date-value-input.tsxDateValueInputCalendar, single day or range
filter-datetime-value-input.tsxDateTimeValueInputCalendar with hour and minute columns, single or range
filter-time-value-input.tsxTimeValueInputHour and minute columns, single or range
filter-picker-field.tsxPickerField, RangePickerField, PickerFooterThe button, the From → To field and Now / OK around those pickers
filter-time-panel.tsxTimeColumnsScrolling hour and minute columns, from the keyboard too
filter-combobox.tsxFilterComboboxSearchable list used by the selects
filter-rule-warnings.tsxFilterRuleWarnings, FilterDraftRuleWarningsWarning icon and popover
filter-rule-summary.tsuseRuleSummaryA rule in words, for chips
filter-date-format.tsformatDateOnly, formatDateTime…Locale-aware date display, hydration-safe
filter-time-parts.ts, filter-picker-field-parts.tsHelpersHours and minutes, range values, shared class names

Custom value inputs

Each row renders the value input through a slot. The default, FilterValueInput, picks one by field type from filterValueInputs. To add an input for a custom field type, or replace a built-in one, pass your own map:

components/filter/my-value-input.tsx
"use client"

import {
  FilterValueInput,
  filterValueInputs,
} from "@/components/filter/filter-value-input"
import type { FilterValueSlotProps } from "@/components/filter/filter-rule-row"

function RatingInput({ id, rule, setValue }: FilterValueSlotProps) {
  const value = typeof rule.value === "number" ? rule.value : 0
  return (
    <div id={id} role="radiogroup" aria-label="Rating" className="flex gap-1">
      {[1, 2, 3, 4, 5].map((star) => (
        <button
          key={star}
          type="button"
          role="radio"
          aria-checked={value === star}
          onClick={() => setValue(star)}
        >
          {star <= value ? "★" : "☆"}
        </button>
      ))}
    </div>
  )
}

const inputs = { ...filterValueInputs, rating: RatingInput }

export function MyValueInput(props: FilterValueSlotProps) {
  return <FilterValueInput {...props} inputs={inputs} />
}
Usage
<FilterBuilder valueInput={MyValueInput} />

Define the component at module scope: rows are memoized, and a new component on every render would remount the inputs.

The slot receives:

PropDescription
idPut it on the first focusable element: it gets focus after an operator is picked.
ruleThe draft rule. rule.value is whatever was last set, possibly partial.
fieldThe field definition.
arity"single", "range" or "multi". Render one input, two, or a multi-select.
setValue(value)Updates the draft. Raw text is fine; it's parsed on apply.

Field types without an entry get a text input, which handles single and range values.

Calendar locale

The Radix UI and Base UI date inputs use the shadcn Calendar (react-day-picker). It shows English month names and weeks starting on Sunday until you pass a locale. Edit filter-date-value-input.tsx:

components/filter/filter-date-value-input.tsx
import { vi } from "react-day-picker/locale"

<Calendar locale={vi} /* … */ />

Chips and the date input's button already format dates with the browser's locale.