Filter Composer Bar
A Linear-style filter composer — pick a field from "Add filter" and the pill chip springs in; the chip's value opens a menu in place, and removing one lets the rest close ranks with FLIP.
npx shadcn@latest add https://webberui.com/r/filter-composer-bar.jsonPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<FilterComposerBar />
Installation
npx shadcn@latest add https://webberui.com/r/filter-composer-bar.jsonOr, once registries are configured in components.json, install it as @webberui/filter-composer-bar.
Usage
import { FilterComposerBar } from "@/components/ui/filter-composer-bar";
<FilterComposerBar
fields={[
{
key: "status",
label: "Status",
options: [
{ value: "active", label: "In progress" },
{ value: "done", label: "Done" },
],
},
{
key: "priority",
label: "Priority",
options: [
{ value: "high", label: "High" },
{ value: "low", label: "Low" },
],
},
]}
onChange={(conditions) => console.log(conditions)}
/>When value is omitted, the component manages the state internally (uncontrolled mode); passing value together with onChange puts it in controlled mode. onChange always receives the latest FilterCondition[] — on add, on value change, and on removal.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
fields | FilterField[] | — | The available fields, each { key, label, options: { value, label }[] }; at most one condition per field at a time |
value | FilterCondition[] | — | Controlled condition array, each { key, value }; when omitted, the component manages it internally |
onChange | (conditions: FilterCondition[]) => void | — | Fires when the conditions change (add, value change, removal) with the latest condition array |
addLabel | string | "加入條件" | Text of the "Add filter" button |
className | string | — | Appended to the outermost container's class |
How it works
- Three-part chip: field name (static) / value (clickable) / remove ×, sliced into one pill by thin dividers — Linear's filter vocabulary
- Spring entrance: once a field is chosen, the chip springs in from below on scale + y, and the new condition defaults to that field's first option
- In-place value change: clicking the chip's value segment expands a small menu with a scaling fade, anchored under that chip; picking an option updates the value text instantly without ever leaving the bar
- FLIP close-ranks: clicking × shrinks that chip out (
AnimatePresence mode="popLayout"takes it out of flow first) while the remaining chips and the "Add filter" button glide into place with a layout FLIP; with many conditions, flex-wrap handles the line breaks - Click-outside to close: the popover is absolutely positioned and anchored, with a
documentpointerdown listener that closes it on an outside click (cleaned up on unmount); only one menu is ever open at a time - At most one condition per field at a time; when every field is used up, the "Add filter" button is disabled
Accessibility
- When the user has "reduce motion" enabled at the system level, the spring entrance, FLIP, and menu scaling are all disabled and state changes are instant (the DOM structure matches SSR)
- Every chip segment is a native
<button>and takes Tab focus in order; the value segment and the "Add filter" button carryaria-haspopup/aria-expandedto expose the menu state - Value menu items are
menuitemradiowitharia-checkedmarking the current value; the remove button carries anaria-labelnaming the field whose condition it removes - Esc closes the menu and focus returns to the trigger; a ring is shown on
focus-visible
Mutation List Choreographer
Full CRUD list choreography — insertions open a gap and drop in, deletions collapse the neighbours, reordering moves continuously with FLIP, batches cascade in waves, and removals leave an undo ghost.
Lasso Select Stage
Drag a lasso box across a grid; the items it catches lift and light up in sequence, and on release the count badge morphs into a floating batch action bar.