Filters

The filters on a list, read as words, so the whole state of the list is always in view.

Try it

Status is Active or Invite pending

Press the words of a chip to change its values; press + to add a field; the × takes a chip away.

Drawn on the product’s staff list, the “future proof” version: a bar above the table with a chip for each field that has a value, a + that opens the fields and then a field’s values, and Clear. It is what Linear, Notion and Airtable converged on, and it makes a filter simple for one reason: the bar says the whole state of the list in words, and nothing is hidden behind an icon. A chip’s words are a button that opens its values again, which is the part that saves the most.

Controlled

value is the caller’s: one entry per field with something chosen, in the order the fields were added, each holding its values in the field’s own order. Every check reports the next value at once, as drawn, and unchecking the last value of a field drops the field. The component filters nothing; the caller does, and a Table’s page shows the two together.

The words

The drawing says “Status is Active and Invite Pending”. Read literally that asks for both, and what is meant is either, so the chip says or; Linear writes “is any of”. describe is for another language, or another logic.

Role é Owner ou Admin

Where it departs from the drawing

The drawn chip is a rectangle at radius 8 in 14px. The chip here is the Tag, a capsule in 12px: “a filter on a list” is what the Tag was written for, and one chip in the system is worth more than a second one that matches a drawing by two pixels. The drawn + opens a menu with a submenu; here it is a Popover with the fields on one side and the picked field’s values on the other, which is the same shape without a second layer to place. And the toolbar’s filter icon is not composed beside the bar: it repeated the +.

Accessibility

The bar is a group named by its label. A chip’s words are a button named by the words, with the popover’s own aria-haspopup; its × is “Remove Status filter”. The values are checkboxes in a fieldset whose legend is the field, so a reader hears “Status, Active, checked”. In the + panel the picked field is aria-pressed. Esc and a click outside close a panel, and the focus goes back to the words or the +, which is the platform’s.

Using it

TSX
import { Filters } from 'alpenglow';
import type { FilterField, FilterValue } from 'alpenglow';

const fields: FilterField[] = [
  { key: 'status', label: 'Status', options: [{ value: 'active', label: 'Active' }, …] },
  { key: 'role', label: 'Role', options: [{ value: 'owner', label: 'Owner' }, …] },
];

const [filters, setFilters] = useState<FilterValue[]>([]);

<Filters fields={fields} value={filters} onChange={setFilters} />

// Then the caller filters: a row passes when, for every entry, its field is one of the values.
rows.filter((row) => filters.every((f) => f.values.includes(row[f.key])));

// The chip's words, in another language or another logic.
<Filters … describe={(field, chosen) => `${field.label}: ${chosen.map((o) => o.label).join(', ')}`} />
Filters props
PropTypeDefault
fieldsFilterField[] — { key, label, options: { value, label }[] }required
valueFilterValue[] — { key, values: string[] }required
onChange(next: FilterValue[]) => voidrequired
labelstring'Filters'
addLabelstring'Add filter'
clearLabelstring'Clear'
describe(field, chosen) => ReactNode"Field is A or B"
classNamestring—