Filter Bar
A row of removable filter chips, an "Add filter" dropdown, and an optional search input — for building list and table filter toolbars.
- Status
- Since
0.1.0- Accessibility pattern
- named ARIA group
Last updated
Install
Add Filter Bar from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.
pnpm dlx shadcn@latest add @vegastack/filter-barThe same command installs the registry items it composes: @vegastack/button, @vegastack/chip, @vegastack/dropdown-menu, @vegastack/input.
Usage
import { FilterBar } from "@/components/ui/filter-bar";
<FilterBar
aria-label="Task filters"
filters={[
{
id: "status",
label: "Status",
value: "In Progress",
onRemove: removeStatus,
},
]}
addFilters={[{ id: "priority", label: "Priority" }]}
onAddFilter={(id) => openFilter(id)}
search={{ value: query, onValueChange: setQuery }}
/>;FilterBar is presentational — it never holds filter state. You own the array of active
filters and update it in each filter's onRemove; you decide what an addFilters option does in
onAddFilter; and search is a controlled value / onValueChange pair.
Anatomy
Filter Bar is a compound component. Every exported part, with the
data-slot it renders (generated from the canonical source):
Examples
Anatomy
The bar lays out three regions left-to-right, all built from VegaStack primitives:
<FilterBar
filters={[/* … */]} // → one removable <FilterChip> per active filter
addFilters={[/* … */]} // → an "Add filter" <DropdownMenu> (Button trigger)
onAddFilter={(id) => {}}
search={{ value, onValueChange }} // → a trailing search <Input type="search">
trailing={/* e.g. a "Clear all" Button */}
/>FilterBar— the container (data-slot="filter-bar",role="group"). A wrapping flex row that reflows its children when space is tight. It defaults toaria-label="Filters"; passaria-labeloraria-labelledbywhen the surrounding UI needs a more specific group name.FilterChip— one active filter (data-slot="filter-chip"): an optional leadingicon, a mutedlabel, an optional emphasizedvaluebeside it, and a trailing×Buttonthat firesonRemove. Rendered once perfiltersentry, and also exported standalone for custom layouts.- Add filter — a
DropdownMenuwhose trigger is a dashed-outlineButton(data-slot="filter-bar-add"). Built from the declarativeaddFiltersarray, or replaced wholesale with theaddFilterMenuslot for submenus / multi-select. - Search — a controlled
Inputoftype="search"(data-slot="filter-bar-search"), pushed to the trailing edge. Omitsearchto hide it.
Filter compositions
With search
A query input is pushed to the trailing edge alongside the chips and the "Add filter" menu.
Empty
With no active filters, only the "Add filter" menu (and search, if enabled) remain.
Presence-only chip & disabled option
A filter without a value renders as a label-only presence chip (the filter is simply on or
off). The "Add filter" menu here also carries a disabled option — it stays visible but is skipped
by arrow-key navigation.
Custom add-filter menu
Pass addFilterMenu to replace the declarative menu wholesale with any DropdownMenu tree — here a
multi-select checkbox group. addFilterMenu takes precedence over addFilters; supply the whole
menu including its trigger.
Standalone chips
FilterChip is exported on its own for custom toolbars. It is the Chip
primitive at the standalone (md, 32px) tier, so it lines up with the Buttons and Inputs beside it.
The example shows an active chip (promoted to the surface-2 selection rung), a plain
active={false} presence chip on the rest fill, and a value-less chip.
Scope (presentational core)
FilterBar is a presentational primitive (Model A — the component owns
rendering; your app owns the filter state). It renders the chips, the add-filter
menu, the search field, and a trailing slot. Stateful/data behavior from the
platform demo is deliberately consumer-owned, not built-in (a recorded scope
decision — requirements §12), and each has a composition path:
- Active-filter state — you hold the filter model and map it to
filters; the component is controlled. - Clear-all — compose a button into
trailing:<FilterBar filters={chips} trailing={ active.length > 0 && ( <Button variant="ghost" size="sm" onClick={clearAll}> Clear all </Button> ) } /> - Editable chip popovers — wrap a
FilterChipin aPopoverto edit a filter's value in place. - AI-suggested filters — an app/data concern (out of the DS primitive); render
your suggestions into the add-filter menu or
trailing.
The component matrix marks FilterBar complete for this presentational-core scope;
the fully-stateful nested builder is FilterBuilder.
API Reference
FilterBar
| Prop | Type | Default | Description |
|---|---|---|---|
addFilterLabel | string | 'Add filter' | Accessible name for the built-in "Add filter" trigger (icon + text button). |
addFilterMenu | React.ReactNode | — | Fully custom "Add filter" menu content (e.g. a DropdownMenu with
submenus / checkbox items). Takes precedence over addFilters — supply the
whole DropdownMenu tree, including its trigger. When omitted and
addFilters is empty, no "Add filter" control is rendered. |
addFilterMenuAlign | Align | 'start' | Alignment of the built-in "Add filter" menu relative to its trigger. |
addFilters | FilterBarAddOption[] | — | Declarative "Add filter" menu options. The bar builds a DropdownMenu
from these and calls FilterBarProps.onAddFilter with the chosen
option's id. Ignored when addFilterMenu is provided. |
filters | FilterBarFilter[] | [] | The active filters, rendered as removable chips at the start of the bar. |
onAddFilter | ((id: string) => void) | — | Invoked with the chosen option's id when an item from the declarative
addFilters menu is selected. |
search | FilterBarSearch | — | Controlled search/query input config. Omit to hide the search field. |
searchInputProps | Omit<InputProps, "onChange" | "placeholder" | "value"> | — | Props forwarded to the underlying search Input. |
trailing | React.ReactNode | — | Content rendered at the trailing (right) end of the bar — e.g. a "Save view" or "Clear all" Button. |
Data attributes and CSS variables on FilterBar
| Attribute | Values |
|---|---|
data-filter-id | mirrors a prop or state value |
data-slot | "filter-bar" | "filter-bar-add" | "filter-bar-search" | "filter-bar-trailing" |
FilterChip
| Prop | Type | Default | Description |
|---|---|---|---|
label* | React.ReactNode | — | The filter's name (muted leading text). |
onRemove* | () => void | — | Invoked when the remove (×) control is activated. |
active | boolean | true | Whether the chip reads as an active selection. An active chip takes the
selection rung (surface-2); an inactive chip keeps a filled control's rest
fill (surface-1). |
icon | React.ReactNode | — | Optional leading icon. |
removeLabel | string | — | Accessible name for the remove control. Defaults to Remove <label> filter
when label is a string. |
value | React.ReactNode | — | Optional value summary, rendered after label separated by a colon. |
Data attributes and CSS variables on FilterChip
| Attribute | Values |
|---|---|
data-slot | "filter-chip" |
FilterBarFilter
The shape of each entry in FilterBar's filters array — one removable chip.
FilterBarFilter
| Prop | Type | Default | Description |
|---|---|---|---|
id* | string | — | Stable identity for the chip (used as the React key and data-filter-id). |
label* | React.ReactNode | — | The filter's name (e.g. "Status"). Rendered as the muted leading text of
the chip. |
onRemove* | () => void | — | Invoked when the chip's remove (×) control is activated. |
active | boolean | true | Whether the chip reads as an active selection (the surface-2 selection
rung). An applied filter is a selection, so this defaults to true; set
false for a presence-only chip on the rest fill. |
icon | React.ReactNode | — | Optional leading icon — a single lucide-react / @vegastack/design/icons element. |
value | React.ReactNode | — | Optional human-readable value summary (e.g. "In Progress" or
"2 selected"). Rendered after label, separated by a colon. Omit for a
presence-only filter. |
FilterBarAddOption
The shape of each entry in the declarative addFilters menu.
FilterBarAddOption
| Prop | Type | Default | Description |
|---|---|---|---|
id* | string | — | Stable identity for the option (used as the React key and passed to onAddFilter). |
label* | React.ReactNode | — | The option's label. |
disabled | boolean | false | Disables the option and removes it from keyboard navigation. |
icon | React.ReactNode | — | Optional leading icon — a single lucide-react / @vegastack/design/icons element. |
FilterBarSearch
The controlled search config passed to FilterBar.
FilterBarSearch
| Prop | Type | Default | Description |
|---|---|---|---|
onValueChange* | (value: string) => void | — | Invoked with the next value on every keystroke. |
value* | string | — | The current query value. |
aria-label | string | — | Accessible name for the search field. Falls back to placeholder, then
'Search'. |
placeholder | string | 'Search…' | Placeholder text shown while the query is empty. Also used as the field's
accessible name when no aria-label is supplied. |
Accessibility
- The bar is a named
role="group"("Filters"by default) so assistive tech announces the filter controls as a single related set. Usearia-label/aria-labelledbyfor a more specific group name like"Task filters". - Each chip's remove control is the shared
ChipRemove: a round ghostIconButtonwhose REAL border box is 24×24 (WCAG 2.5.8), with an accessible name —Remove <label> filterwhenlabelis a string, overridable viaFilterChip'sremoveLabel. - The "Add filter" trigger is a labelled
Buttonthat opens a keyboard-navigableDropdownMenu; arrow keys move between options and Esc closes it. - The search field always has an accessible name — it falls back to the
placeholder, thenSearch. Providearia-labelonsearchfor a name distinct from the placeholder. - Every interactive part keeps a visible
:focus-visiblering — neveroutline: none.
| Key | Action |
|---|---|
| Tab | Move focus through the chips' remove buttons, the "Add filter" trigger, and the search field. |
| Enter / Space | Remove the focused chip, or open the "Add filter" menu. |
| ↑ / ↓ | Move between options in the open "Add filter" menu. |
| Esc | Close the open "Add filter" menu. |
| Contract | States tested |
|---|---|
| Behaviour | default, active, disabled, empty, selected |
| Accessibility | disabled, labeled, semantic-html |
| Visual | default, hover, active, empty |