Skip to content
Component installs need the registry setup— the Base UI shadcn project, the @vegastack namespace and the Cloudflare Access service token.
VegaStack Design

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
stable
Since
0.1.0
Accessibility pattern
named ARIA group

Last updated

StatusIn ProgressPriorityHighAssigneeAny

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-bar

The 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):

FilterBar — data-slot="filter-bar" | "filter-bar-add" | "filter-bar-search" | "filter-bar-trailing"
FilterChip — data-slot="filter-chip"

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 to aria-label="Filters"; pass aria-label or aria-labelledby when the surrounding UI needs a more specific group name.
  • FilterChip — one active filter (data-slot="filter-chip"): an optional leading icon, a muted label, an optional emphasized value beside it, and a trailing × Button that fires onRemove. Rendered once per filters entry, and also exported standalone for custom layouts.
  • Add filter — a DropdownMenu whose trigger is a dashed-outline Button (data-slot="filter-bar-add"). Built from the declarative addFilters array, or replaced wholesale with the addFilterMenu slot for submenus / multi-select.
  • Search — a controlled Input of type="search" (data-slot="filter-bar-search"), pushed to the trailing edge. Omit search to hide it.

Filter compositions

A query input is pushed to the trailing edge alongside the chips and the "Add filter" menu.

Labelbug

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.

StarredStatusIn Progress

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.

Labelsbug

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.

StatusIn ProgressPriorityHighStarred

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 FilterChip in a Popover to 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

PropTypeDefaultDescription
addFilterLabelstring'Add filter'Accessible name for the built-in "Add filter" trigger (icon + text button).
addFilterMenuReact.ReactNodeFully 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.
addFilterMenuAlignAlign'start'Alignment of the built-in "Add filter" menu relative to its trigger.
addFiltersFilterBarAddOption[]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.
filtersFilterBarFilter[][]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.
searchFilterBarSearchControlled search/query input config. Omit to hide the search field.
searchInputPropsOmit<InputProps, "onChange" | "placeholder" | "value">Props forwarded to the underlying search Input.
trailingReact.ReactNodeContent 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

AttributeValues
data-filter-idmirrors a prop or state value
data-slot"filter-bar" | "filter-bar-add" | "filter-bar-search" | "filter-bar-trailing"

FilterChip

PropTypeDefaultDescription
label*React.ReactNodeThe filter's name (muted leading text).
onRemove*() => voidInvoked when the remove (×) control is activated.
activebooleantrueWhether 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).
iconReact.ReactNodeOptional leading icon.
removeLabelstringAccessible name for the remove control. Defaults to Remove <label> filter when label is a string.
valueReact.ReactNodeOptional value summary, rendered after label separated by a colon.

Data attributes and CSS variables on FilterChip

AttributeValues
data-slot"filter-chip"

FilterBarFilter

The shape of each entry in FilterBar's filters array — one removable chip.

FilterBarFilter

PropTypeDefaultDescription
id*stringStable identity for the chip (used as the React key and data-filter-id).
label*React.ReactNodeThe filter's name (e.g. "Status"). Rendered as the muted leading text of the chip.
onRemove*() => voidInvoked when the chip's remove (×) control is activated.
activebooleantrueWhether 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.
iconReact.ReactNodeOptional leading icon — a single lucide-react / @vegastack/design/icons element.
valueReact.ReactNodeOptional 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

PropTypeDefaultDescription
id*stringStable identity for the option (used as the React key and passed to onAddFilter).
label*React.ReactNodeThe option's label.
disabledbooleanfalseDisables the option and removes it from keyboard navigation.
iconReact.ReactNodeOptional leading icon — a single lucide-react / @vegastack/design/icons element.

FilterBarSearch

The controlled search config passed to FilterBar.

FilterBarSearch

PropTypeDefaultDescription
onValueChange*(value: string) => voidInvoked with the next value on every keystroke.
value*stringThe current query value.
aria-labelstringAccessible name for the search field. Falls back to placeholder, then 'Search'.
placeholderstring'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. Use aria-label/aria-labelledby for a more specific group name like "Task filters".
  • Each chip's remove control is the shared ChipRemove: a round ghost IconButton whose REAL border box is 24×24 (WCAG 2.5.8), with an accessible name — Remove <label> filter when label is a string, overridable via FilterChip's removeLabel.
  • The "Add filter" trigger is a labelled Button that opens a keyboard-navigable DropdownMenu; arrow keys move between options and Esc closes it.
  • The search field always has an accessible name — it falls back to the placeholder, then Search. Provide aria-label on search for a name distinct from the placeholder.
  • Every interactive part keeps a visible :focus-visible ring — never outline: none.
KeyAction
TabMove focus through the chips' remove buttons, the "Add filter" trigger, and the search field.
Enter / SpaceRemove the focused chip, or open the "Add filter" menu.
/ Move between options in the open "Add filter" menu.
EscClose the open "Add filter" menu.
ContractStates tested
Behaviourdefault, active, disabled, empty, selected
Accessibilitydisabled, labeled, semantic-html
Visualdefault, hover, active, empty

Do / Don't

Do
Keep filter state in the host and reflect it through filters/onRemove and onAddFilter — show a chip for every active filter so users can see and remove each one.
Don't
Put filtering logic inside the bar or render an 'Add filter' option for a filter that's already active — hide active options from the menu instead.

On this page