WebberUI

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.

Loading preview…
npx shadcn@latest add https://webberui.com/r/filter-composer-bar.json

Playground

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.json

Or, 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

PropTypeDefaultDescription
fieldsFilterField[]The available fields, each { key, label, options: { value, label }[] }; at most one condition per field at a time
valueFilterCondition[]Controlled condition array, each { key, value }; when omitted, the component manages it internally
onChange(conditions: FilterCondition[]) => voidFires when the conditions change (add, value change, removal) with the latest condition array
addLabelstring"加入條件"Text of the "Add filter" button
classNamestringAppended 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 document pointerdown 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 carry aria-haspopup / aria-expanded to expose the menu state
  • Value menu items are menuitemradio with aria-checked marking the current value; the remove button carries an aria-label naming the field whose condition it removes
  • Esc closes the menu and focus returns to the trigger; a ring is shown on focus-visible

On this page