Filters
The filters on a list, read as words, so the whole state of the list is always in view.
Try it
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.
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
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(', ')}`} />| Prop | Type | Default |
|---|---|---|
fields | Filter | required |
value | Filter | required |
onChange | (next: Filter | required |
label | string | 'Filters' |
addLabel | string | 'Add filter' |
clearLabel | string | 'Clear' |
describe | (field, chosen) => React | "Field is A or B" |
className | string | — |