Filter Toolbar
The filter row of a list page: a search box, the most used filter inline, the rest behind "Filters" — a popover on a wide screen, a bottom sheet on a phone — and what applies as removable chips. Values in, changes out, so it works over state, a store or the URL.
src/components/ui/filter-toolbar.tsxAbout
From the component's own source, where it was written.
The filter pattern of a list page, in one row.
A search box takes the free width; the filter used most sits inline beside it — a SegmentedControl, a Select; the rest go behind "Filters", in a popover on a wide screen and a bottom sheet on a phone; and whatever applies shows underneath as chips that can each be removed. Every list that grows filters one at a time ends up with a row of eight selects that wraps into three lines on a laptop. This is the shape it should have grown into.
Nothing here knows where filter state lives. Values come in and changes go out, so it works the same over component state, a store or the URL — useFilterParams is the URL case, written against a plain URLSearchParams so any router can supply one.
Install
npx astralyx-ui add filter-toolbar
Usage
import { FilterField, FilterToolbar } from '@/components/ui/filter-toolbar'
<FilterToolbar
search={{ value: q, onChange: setQ, placeholder: 'Search tickets' }}
quick={<SegmentedControl label="Status" options={statuses} value={status} onValueChange={setStatus} />}
panel={
<FilterField label="Region">
<Select options={regions} value={region} onValueChange={setRegion} />
</FilterField>
}
panelCount={region ? 1 : 0}
chips={chips}
onReset={reset}
/>Examples
Over component state
<FilterToolbar search={…} quick={…} panel={…} chips={chips} onReset={reset} />In the URL
const [params, setParams] = useSearchParams()
const filters = useFilterParams(['status', 'q'], {
params,
onParamsChange: (next) => setParams(next, { replace: true }),
checks: { status: filterChecks.oneOf(['open', 'closed']) },
})
filters.values.status // '' when unset or invalid
filters.set('status', 'open') // and back to the first pageAPI
Generated from the source, so it cannot fall behind the code. One table per exported component, then hooks, functions and types.
FilterToolbar
Extends Omit<FilterPanelProps, 'activeCount' | 'onReset' | 'children'>, Omit<FilterChipsProps, 'chips' | 'onClearAll' | 'onEmpty' | 'className'>.
| Prop | Type | Default | Description |
|---|---|---|---|
| search | { value: string onChange: (value: string) => void placeholder?: string /** Accessible name. Defaults to `searchLabel`. */ label?: string /** Monospace input, for ids and hashes. */ mono?: boolean maxLength?: number } | — | The search box. It takes the free width of the row and wraps above the rest on a narrow screen. |
| quick | ReactNode | — | The one filter used most, inline. Scrolls sideways rather than wrapping when the row is tight. |
| panel | ReactNode | — | The rest, behind the Filters button, as FilterField each. A popover from sm up and a bottom sheet below — on a phone a popover is a screen-wide box whose Done button sits under the keyboard. |
| panelCount | number | 0 | How many of the panel's filters apply. Shown on the button, so a filter hidden in the panel is never forgotten. |
| chips | FilterChip[] | [] | { key, label, onRemove } for each filter that applies, wherever it lives. Removing one keeps focus in the row — on the next chip, then Clear all, then the search box. |
| onReset | () => void | required | Clears every filter. Behind Reset in the panel and Clear all under the chips. |
| className | string | — | — |
| searchLabel | string | 'Search' | Visible and accessible text, in English by default. |
| filtersLabel | string | — | Visible and accessible text, in English by default. |
| activeCountLabel | (count: number) => string | — | Accessible names that carry a value: "Remove Status: open", "2 active". |
| resetLabel | string | — | Visible and accessible text, in English by default. |
| doneLabel | string | — | Visible and accessible text, in English by default. |
| closeLabel | string | — | The sheet's close button, on a phone. |
| clearAllLabel | string | — | Visible and accessible text, in English by default. |
| removeLabel | (label: string) => string | — | Accessible names that carry a value: "Remove Status: open", "2 active". |
| activeLabel | string | — | Accessible name of the group. |
FilterField
A caption over one control in the panel. htmlFor makes it a real label; wide spans both columns.
| Prop | Type | Default | Description |
|---|---|---|---|
| label | ReactNode | required | — |
| htmlFor | string | — | The control's id, so the label is a real <label>. |
| wide | boolean | false | — |
| className | string | — | — |
| children | ReactNode | required | — |
FilterPanel
The two halves on their own, for a layout the toolbar does not fit.
| Prop | Type | Default | Description |
|---|---|---|---|
| activeCount | number | required | How many of the panel's filters apply — shown as a count on the button. |
| onReset | () => void | required | Clears every filter. Behind Reset in the panel and Clear all under the chips. |
| children | ReactNode | required | — |
| filtersLabel | string | 'Filters' | Visible and accessible text, in English by default. |
| activeCountLabel | (count: number) => string | DEFAULT_ACTIVE_COUNT | Accessible names that carry a value: "Remove Status: open", "2 active". |
| resetLabel | string | 'Reset' | Visible and accessible text, in English by default. |
| doneLabel | string | 'Done' | Visible and accessible text, in English by default. |
| closeLabel | string | 'Close' | The sheet's close button, on a phone. |
FilterChips
The two halves on their own, for a layout the toolbar does not fit.
| Prop | Type | Default | Description |
|---|---|---|---|
| chips | FilterChip[] | required | { key, label, onRemove } for each filter that applies, wherever it lives. Removing one keeps focus in the row — on the next chip, then Clear all, then the search box. |
| onClearAll | () => void | required | — |
| onEmpty | () => void | — | Where focus goes when the last chip is removed — the search field, say. |
| className | string | — | — |
| clearAllLabel | string | 'Clear all' | Visible and accessible text, in English by default. |
| removeLabel | (label: string) => string | DEFAULT_REMOVE | Accessible names that carry a value: "Remove Status: open", "2 active". |
| activeLabel | string | 'Active filters' | Accessible name of the group. |
Hooks
| Hook | Signature | Description |
|---|---|---|
| useFilterParams | <K extends string>(keys: readonly K[], { params, onParamsChange, checks = {}, }: { params: URLSearchParams onParamsChange: (next: URLSearchParams) => void checks?: Partial<Record<K, FilterCheck>> }) => { values: Record<K, string>; offset: number; set: (key: 'offset' | K, value: FilterValue) => void; setMany: (patch: Partial<Record<'offset' | K, FilterValue>>) => void; clear: () => void; active: boolean; } | Filters in the query string, against a plain URLSearchParams so any router can supply one. Changing a filter drops offset; clear removes only its own keys. |
Functions
| Function | Signature | Description |
|---|---|---|
| FILTER_ANY | '__any' | "Any" for a Select, which cannot hold an empty value. Treated as unset. |
| filterChecks | { oneOf: (options: readonly string[]) => FilterCheck; matches: (pattern: RegExp) => FilterCheck; maxLength: (max: number) => FilterCheck; uuid: FilterCheck; isoDateTime: FilterCheck; flag: FilterCheck; } | Ready-made checks for useFilterParams. |
| parseOffset | (raw: string | null) => number | ?offset= as a safe whole number. Anything else is the first page. |
Types
FilterChip
One applied filter, as a removable chip: "Status: failed ×".
| Field | Type | Description |
|---|---|---|
| key | string | — |
| label | string | — |
| onRemove | () => void | — |
| Type | Definition | Description |
|---|---|---|
FilterCheck | (value: string) => boolean | Checks one value read from the URL. A value that fails reads as unset. |
Behaviour
| Topic | Detail | Description |
|---|---|---|
| checks | Partial<Record<key, FilterCheck>> | A link is input from anyone. A value that fails its check reads as unset and is removed from the URL. filterChecks has oneOf, matches, maxLength, uuid, isoDateTime and flag. |