Filter Bar

Search, filter controls built from definitions, active-filter chips, and Apply and Reset, above a table, list or dashboard.
Stable
Web
iOS
Android
@advui/data
Source
JSX
<FilterBar filters={usersFilters} defaultValue={{ status: 'active', verified: false }} />
sample
layout
size
variant

Installation

pnpm
pnpm add @advui/data
npm
npm install @advui/data

Usage

import { FilterBar, type FilterState } from '@advui/data'

const [filters, setFilters] = useState<FilterState>({})

<FilterBar
  searchable
  filters={[
    {
      key: 'status',
      label: 'Status',
      type: 'select',
      options: [
        { label: 'Active', value: 'active' },
        { label: 'Inactive', value: 'inactive' },
      ],
    },
    { key: 'created', label: 'Created', type: 'date-range' },
  ]}
  value={filters}
  onValueChange={setFilters}
  showActiveFilters
  showResetButton
/>

Examples

Search and a status filter

Uncontrolled: the bar keeps its own state.

Filtering a data table

Controlled. The app filters the rows with the state; `ghost` sits flush above the table.
Department
Status
Verified
Aye Chan
Engineering
Active
Yes
Min Thu
Finance
Active
No
Su Myat
Operations
Pending
No
Kyaw Zin
Engineering
Inactive
Yes
Hnin Wai
Finance
Active
Yes
Thura Aung
Operations
Active
Yes
1–6 of 6

Multi-select filter

Picks show as chips in the field, removable one by one; a disabled option cannot be picked.
Engineering
Finance
Department: Engineering, Finance

Date range with Apply and Reset

Changes are a draft until Apply. Reset puts the defaults back and applies at once.
Change a filter, then press Apply.

Active filters and count

Chips remove one filter each; the count and Reset follow. A Verified filter set to No counts.
Engineering
Finance
Operations
4 filters applied
Status: Active
Department: Engineering, Finance +1
Verified: No
Joined: Oct 1, 2026

Dashboard filters

`layout="responsive"`: stacked on phones, a row from `md`, on a `filled` surface.
1 filter applied

Leading and trailing content

A title at the start and an Export button after the filters.

Users

Advanced filters

Filters marked `advanced` wait behind "More filters", which counts the active ones; closing keeps their values.

Custom filter control

A Number Input as a typed `custom` filter, with its own chip text.
Minimum amount: at least 100

Compact, without labels

`showLabels={false}` and `size="sm"`: one line, the labels name the controls.

Variants

`default`, `outline`, `filled` and `ghost` style the container only.

API reference

FilterBar

Also takes Stack style props (`width`, `margin`…), which win over the variant and size.
PropTypeDefaultDescription
filtersFilterDefinition[][]The filters, in order: each one a labelled control. Keys must be unique and differ from `searchKey`; a repeated key logs a warning in development and only its first use is rendered. Definitions are never changed.
valueFilterState—Each filter value by key (controlled). A missing key reads as the filter default; `null` is unset. Keys that are not filters are kept and passed back. With `showApplyButton`, the applied filters.
defaultValueFilterStateevery filter defaultThe starting state when uncontrolled, read once: a new object with the same content does not reset it.
onValueChange(value: FilterState) => void—Called with the whole state, every filter key present: on each change, or with `showApplyButton` on Apply. Reset and chip removal call it too.
onApply(value: FilterState) => void—Apply was pressed (or Enter in the search or a text filter) with `showApplyButton`; gets the state just applied.
onReset() => void—Reset was pressed, once per press, after the filters went back to their defaults. Refresh your data here if you need to; Filter Bar fetches nothing.
searchablebooleanfalseAdds a search field. Its text is in the state under `searchKey`.
searchKeystring'search'The key of the search text in the state: the one place it is kept. `null` when empty.
searchPlaceholderstring'Search'Placeholder of the search field.
layout'horizontal' | 'vertical' | 'responsive''horizontal'`horizontal`: a row of controls that wraps. `vertical`: a column of full-width controls, for a side panel. `responsive`: a column on phones and a row from the `md` breakpoint.
size'sm' | 'md' | 'lg''md'Gap, padding, control widths, and the size of every control and button.
variant'default' | 'outline' | 'filled' | 'ghost''default'The container only: `default` the page background, `outline` adds a border, `filled` a muted surface, `ghost` no fill, border or padding (above a table or inside a card).
wrapbooleantrueIn a row, controls that do not fit move onto new lines. Turn off only when the bar always has room.
showLabelsbooleantrueVisible labels above the controls. When false the labels still name the controls, and the bar is one line high.
showApplyButtonbooleanfalseChanges stay a draft until Apply: `value` and `onValueChange` are then the applied state. Apply is disabled while nothing changed or a range is invalid.
showResetButtonbooleanfalseA Reset button that puts every filter and the search back to their defaults (the "clear all" action). Disabled when there is nothing to reset.
showActiveCountbooleanfalseA badge with the number of active filters ("2 filters applied"), hidden at zero.
showActiveFiltersbooleanfalseActive filters as chips ("Status: Active"); each chip removes its filter (back to its default).
collapsiblebooleanfalsePuts the filters marked `advanced` behind a "More filters" button that counts the active ones. With none marked, every filter goes behind it.
disabledbooleanfalseDisables every control, chip and action.
leadingReactNode—Content at the start, such as a title. A string is shown as Text.
trailingReactNode—Content at the end, after the actions, such as an Export button.
localestringthe device'sLocale of the date pickers and of the dates in chips.
labelsPartial<FilterBarLabels>—Every text Filter Bar writes (Apply, Reset, "More filters", the count, remove and clear button names, "All", "Yes", "No"…), for translation.
aria-labelstring'Filters'Names the bar, a `group`. Name it when a page has several.
gapSpaceTokens | numberfrom `size`Space between controls and between rows.
paddingSpaceTokens | numberfrom `size`, none for `ghost``paddingHorizontal`, `paddingVertical` and the shorthands work too.
backgroundColorColorTokensfrom `variant`Overrides the variant surface.
borderColorColorTokens'border' (outline)Overrides the outline color. Other variants show it with a `borderWidth`.
borderRadiusRadiusTokens | number'lg'Corner radius of the container.

FilterDefinition

One filter. `type` picks the control: `select` (Select), `multi-select` (Multi Select), `boolean` (a Select of All, Yes and No), `date` (Date Picker), `date-range` (Date Range Picker), `text` (Input) or `custom`.
PropTypeDefaultDescription
key requiredstring—Unique and stable: the filter key in the state.
label requiredstring—Names the control and starts its chip.
type required'select' | 'multi-select' | 'boolean' | 'date' | 'date-range' | 'text' | 'custom'—The control, and the shape of the value (see Values).
optionsFilterOption[]—`select` and `multi-select`: `{ label, value, disabled? }`. Values may be strings or numbers (and booleans in a select); they go into the state as given, never their labels.
defaultValueFilterValuenullWhat a missing key reads as, what Reset and a chip restore, and a value that is not counted as active.
placeholderstring—Shown while unset. For `select` and `boolean` also the first option, which unsets the filter. Default "All" for those and `multi-select`.
trueLabelstring'Yes'`boolean`: the option and chip text for `true`.
falseLabelstring'No'`boolean`: the option and chip text for `false`.
advancedbooleanfalseWith `collapsible`, the filter sits behind "More filters".
disabledbooleanfalseDisables this filter.
render(context: CustomFilterContext) => ReactNode—`custom`: the control. The context has `value`, `onValueChange`, `reset`, `size`, `disabled` and, while labels are hidden, `aria-label`. Core form controls inside take the label from the filter Field.
format(value) => string—`custom`: how the value reads in its chip. Default: the value as text.

Values

Unset is always null: an emptied text field, an empty multi-select, and a range with neither end are reported as null. false is a value, not unset.

TypeValueCounted and shown as a chip when
selectthe option value (string, number or boolean)set and not its default
multi-selectan array of option values, no repeatsone or more picked; counts once
booleantrue or falseeither one, false included, unless it is the default
datea Date at local midnight, as Calendar gives it; never converted to UTCset
date-range{ start: Date | null, end: Date | null }; one end may be openeither end set; counts once
textthe text as typed (no trimming or case changes)not empty
customany FilterValueset and not its default
searchthe text under searchKeynever: it has its own field and clear button

Apply, Reset and chips

Filter Bar never fetches or queries: it reports the state, and your app filters its data.

ActionWithout Apply buttonWith `showApplyButton`
Change a controlonValueChange at onceChanges the draft only
Apply—onValueChange and onApply with the draft
ResetDefaults for every filter and the search; onValueChange, then onResetThe same, applied at once; the draft is reset too
Remove a chipThat filter back to its default; onValueChangeThe same, applied at once; other pending edits stay
Parent sets valueShownShown, and replaces the draft (when the content differs)

Changing definitions

ChangeWhat happens
A filter is addedIts key reads as its default until it is set.
A filter is removedIts value stays in the state like any other key, and is no longer shown, counted or reset.
Options changeThe value is kept. A value no longer among the options shows the placeholder in the control and its raw value in the chip.
A default changesReset and the count use the new default; the current state is not changed.

Accessibility

  • The bar is a group named by aria-label ("Filters" by default). Every control keeps its own Tab stop and its own keyboard behavior; Filter Bar adds no arrow-key handling.
  • Each filter is a Field: its visible label is linked to the control, or, with showLabels={false}, names it with aria-label. Select, Multi Select, Date Picker and Date Range Picker keep their own roles, states and keyboard support; disabled options are announced as disabled.
  • The search field is a named searchbox with its own clear button. Text and date filters get a clear button ("Clear Created") once they have a value; clearing moves focus back to the control.
  • Chips sit in a group named "Active filters". Each remove button is named from the chip ("Remove Status: Active"). On web, removing one moves focus to the chip that took its place, or to the bar when none is left.
  • The count is text ("2 filters applied"), not a color or a dot. The "More filters" button has aria-expanded and aria-controls, and its name includes the count of active filters behind it.
  • A date range that ends before it starts is marked invalid (aria-invalid) with an error linked to the field; it is never swapped. Apply, Reset and Clear are disabled, not hidden, when they would do nothing.
  • Nothing is a live region: the controls announce their own changes, so the bar adds no second announcement.
Keyboard interactions
KeyAction
TabMoves through the search field, each filter (and its clear button), the chips, and the actions.
EnterIn the search field or a text filter with `showApplyButton`: applies. On a chip remove button: removes the filter.
EscapeIn the search field: clears it (web). Closes an open list or calendar.

Responsive behavior

In a row, each control has a width from size (wider for multi-select and date-range filters), never wider than the bar, and the search field takes the free space. With wrap (on by default) controls that do not fit move onto new lines, and the actions stay at the end edge. layout="vertical" stacks full-width controls for a side panel or sheet, and layout="responsive" does so below the md breakpoint (768px) and switches to a row above it. Chips wrap. Nothing has a fixed height, so long labels and larger text grow the bar instead of being clipped. For many filters on phones, mark the less used ones advanced and set collapsible.

Platform notes

Web
A div with role="group". Selects open a listbox, and the date pickers a popover. Removing a chip moves focus (DOM focus).
iOS
Selects and the multi-select open bottom sheets; on a phone the multi-select field is one button, and picks are removed by unchecking them in the sheet (or with the bar chips). The container is not one accessible element, so VoiceOver reaches each control.
Android
Same as iOS, for TalkBack; the back button closes an open sheet or calendar. Focus is not moved after a chip is removed: TalkBack moves on by itself.
Since v0.13.0Last updated Nov 2, 2026Edit this page on GitHub