Skip to content
Component installs need the registry setup
VegaStack Design

Filter Row

The chips under a ListToolbar — + Filter, dashed pinned facets, the other chips and Clear, plus a line saying what an AI filter run added.

Status
stable
Since
0.25.100
Accessibility pattern
named group; AI chips marked; polite AI note

Last updated

Category: DownlightsIP rating: IP65Finish: Black
2 filters added by AICouldn't match “warehouse”

Install

Configure registry credentials first using the Quickstart. Before copying this item or any transitive registry dependency, verify the signed manifest and save the exact bytes for Filter Row before installation. Retain the digest printed by this command.

pnpm exec vegastack-design verify filter-row

Install with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.

pnpm dlx shadcn@4.21.0 add @vegastack/filter-row

Run the exact offline --post-write command printed by the preflight, using its saved item path and independently retained --expected-integrity digest. Use the saved preflight for every transitive registry dependency too; stop on any mismatch before using the components.

The same command installs the registry items it composes: @vegastack/button, @vegastack/calendar, @vegastack/chip, @vegastack/date-time, @vegastack/list-toolbar, @vegastack/people-picker, @vegastack/popover, @vegastack/searchable-select.

Usage

import { AddFilterMenu } from "@/components/ui/add-filter-menu";
import { FilterRow, ListFacet } from "@/components/ui/filter-row";

<ListToolbar
  filterRow={
    <FilterRow
      addFilter={<AddFilterMenu fields={fields} onPick={addFilter} ai={ai} />}
      facets={
        <ListFacet
          label="Status"
          multiple
          items={statuses}
          value={status}
          onValueChange={setStatus}
        />
      }
      chips={chips}
      aiNote={note}
      onClear={clearAll}
    />
  }
/>;

The order never changes, so chips never jump:

  1. + Filter (addFilter), pinned first — also on a phone, where the row scrolls.
  2. Pinned facets (facets), in the page's order. Unset, a facet is a dashed outline that reads as an offer (+ Due); set, it is a solid chip. A facet with a custom editor is an unset FilterChip (below).
  3. Other chips (chips), in the order they were added — from + Filter, a preset or the AI. A preset resolves to ordinary chips; there is no separate preset chip.
  4. Clear, at the far end, only while something is set.

Chips made by an AI filter (source: "ai") lead with ✦ until the filter is edited. After an AI run the host passes aiNote: one line under the row — "✦ 2 filters added by AI · Undo · Couldn't match “warehouse”" — until the next manual change or its ×. Undo removes exactly that run's chips. Unknown words are listed, never guessed.

From an @3xl row the chips wrap; below it they are one sideways-scrolling line with an edge fade, + Filter and Clear pinned at its two ends. The row counts what is applied (its chips plus its set facets) and tells the enclosing ListToolbar, whose Filter tool shows the count.

Anatomy

Filter Row is a compound component: the row, and the facet chips that sit in it. Every exported part, with the data-slot it renders (generated from the canonical source):

FilterRow — data-slot="filter-row" | "filter-row-add" | "filter-row-ai-dismiss" | "filter-row-ai-note" | "filter-row-ai-undo" | "filter-row-ai-unmatched" | "filter-row-chips" | "filter-row-clear" | "filter-row-scroll"
DateRangeFilter — data-slot="date-range-filter" | "date-range-filter-clear" | "date-range-filter-content" | "date-range-filter-presets" | "date-range-filter-trigger"
ListFacet — data-slot="list-facet" | "list-facet-clear" | "list-facet-remove" | "list-facet-select"
FilterChip — data-slot="filter-chip" | "filter-chip-ai" | "filter-chip-trigger"
PeopleFacet — data-slot="list-facet" | "list-facet-clear" | "list-facet-remove" | "list-facet-select-trigger"
FilterFacetReporter

Examples

Category: DownlightsIP rating: IP65Finish: Black
2 filters added by AICouldn't match “warehouse”

API Reference

PropTypeDefaultDescription
activeCountnumber—The number of filters applied. Omitted, the row counts its chips and its set facets (and tells the enclosing ListToolbar, whose Filter tool shows it).
addFilterReact.ReactNode—+ Filter — an AddFilterMenu. Always first.
aiNoteFilterRowAiNote—The line under the row after an AI filter run.
chipsreadonly FilterRowChip[][]The other applied filters, in the order they were added.
facetsReact.ReactNode—The page's pinned facets, in its order: ListFacet, PeopleFacet, DateRangeFilter, or an unset FilterChip that opens its own editor.
labelsPartial<FilterRowLabels>EnglishThe component's words.
onClear(() => void)—Clear every filter; Clear shows while anything is set.

Data attributes and CSS variables on FilterRow

AttributeValues
data-filter-idmirrors a prop or state value
data-slot"filter-row" | "filter-row-add" | "filter-row-ai-dismiss" | "filter-row-ai-note" | "filter-row-ai-undo" | "filter-row-ai-unmatched" | "filter-row-chips" | "filter-row-clear" | "filter-row-scroll"

Chip

PropTypeDefaultDescription
id*string—Stable id.
label*React.ReactNode—The filter's name ("Priority").
onRemove*() => void—Remove the filter.
defaultEditorOpenbooleanfalseOpen the editor as the chip mounts — a filter just picked from + Filter.
editorReact.ReactNode—The editor that opens from the chip (a value picker).
iconReact.ReactNode—A leading icon.
source"ai" | "user""user"ai when an AI filter made it (a ✦ until it is edited).
valueReact.ReactNode—The value summary ("High").

AI note

PropTypeDefaultDescription
count*number—How many filters it added.
onUndo*() => void—Remove exactly the filters that run added.
onDismiss(() => void)—Hide the line (the next manual change does it too — the host decides).
unmatchedreadonly string[][]The words it could not turn into a filter — listed, never guessed.

ListFacet

One filter on SearchableSelect, in the row's one chip shape: compact (h-7), rounded-md, quiet type. Unset it reads just its label (Status ▾); set it is tinted and reads Status: Open, then Status: 2 from two values (the values in the chip's tooltip and ticked in the popover), with a × named "Clear Status" in the chevron's fixed slot. Use short, sentence-case labels; never "Any" or "All statuses". The search box shows only when the list is worth searching (more than 7 options, or a remote list); searchable forces it on or off.

It passes SearchableSelect's props through (minus the trigger face it owns: renderItem, renderValue, renderTriggerValue, placeholder, variant, size, aria-label, groupBy and groupOrder) — multiple, remote with useAsyncSearch, leadingItems, itemToDescription — and adds pinSelected (the chosen options first, under "Selected") and removable with onRemove. The tick matches by itemToKey, so options from a server search tick as static ones do. For people, pass itemToSecondaryLabel={(p) => p.email} (and itemToAvatar): each option is one PersonOption (the person contract).

PropTypeDefaultDescription
label*string—The facet's name, in sentence case ("Status", "Due date"). An unset facet reads just this; a set one reads "{label}: {value}".
anyLabelstring—Deprecated.
moreGroupLabelstring"More"The heading of the group under the pinned one.
onRemove(() => void)—Called when a removable facet is removed or cleared.
pinSelectedbooleanfalseList the selected options first, under "Selected", above the rest under "More".
removablebooleanfalseA secondary facet added from "More": clearing it also calls onRemove, and while it is unset a remove control sits beside it.
removeLabelstring`Remove ${label} filter`Accessible name of the remove control beside an unset removable facet.
renderItem((item: Item) => React.ReactNode)itemToStringLabelRenders one option. Not used with the person contract (itemToSecondaryLabel / itemToAvatar), which draws avatar, name and email once; passing both warns in development.
searchableboolean—Show the search box in the panel. Unset, it shows only when the list is worth searching: more than 7 options, or a remote (server-searched) list. Pass true or false to override.
selectedGroupLabelstring"Selected"The heading of the pinned group.

PeopleFacet

A people filter: the facet chip ("Assignee", "Assignee: Asha Rao", "Assignee: 2") over the one people picker (PeoplePickerContent), so a filter lists people exactly as every other picker does. leadingOptions adds rows such as "Unassigned".

PropTypeDefaultDescription
label*string—The facet's name, in sentence case ("Assignee", "Recorded by").
onValueChange*(value: PeoplePickerValue<Multiple>) => void—Called with the new choice (null, or [] with multiple, when cleared).
value*PeoplePickerValue<Multiple>—The chosen person — or people, with multiple (controlled).
actionsreadonly PeoplePickerAction[][]Static actions in the footer under the results.
clearLabelstring`Clear ${label}`The clear control's accessible name.
countLabel((n: number) => string)(n) => String(n)The chip's count for several people.
defaultSearchstring—The search text the list starts from instead of "" — the character that opened a cell editor (type-to-edit). PeoplePicker also opens with it, once, on mount.
disabledbooleanfalseDisable the facet.
emptyTextstring"No people found"Shown when nothing matches.
excludereadonly string[][]Ids never offered (a member cannot manage themselves).
headingstring—A label over the rows ("Your agents").
inactiveLabelstring"Inactive"The badge on an inactive person (with includeInactive).
includeInactivebooleanfalseList people with active: false, badged "Inactive".
leadingOptionsreadonly PeoplePickerOption[][]Options always listed first and never filtered out — "Unassigned" in a filter. Give each an icon.
multipleMultiplefalseFilter by several people.
onOpenChange((open: boolean) => void)—Called when the popover opens or closes — start a remote search on the first open.
onRemove(() => void)—Called when a removable facet is removed or cleared.
optionsreadonly PeoplePickerOption[]—A fixed list instead of search, filtered here by name, email and description.
removablebooleanfalseA secondary facet added from "More": clearing it also calls onRemove.
removeLabelstring`Remove ${label} filter`The remove control's accessible name.
searchPeoplePickerSearch—Finds people and teams for the typed query ("" on open), debounced and race-safe; return nextCursor to page on scroll. Leave out anyone who cannot be chosen.
searchPlaceholderstring"Search people"The search field's placeholder and accessible name.
selectedreadonly string[][]Ids shown with a check (the current choice).
teamLabel((memberCount: number) => string)(n) => `Team · ${n} ${n === 1 ? "person" : "people"}`A team's second line.
viewerIdstring | null—The viewer's id: their row reads "(you)" and is listed first.
youLabelstring"(you)"The viewer's marker after their name.

Data attributes and CSS variables on PeopleFacet

AttributeValues
data-placeholder""
data-slot"list-facet" | "list-facet-clear" | "list-facet-remove" | "list-facet-select-trigger"
data-state"set" | "unset"

DateRangeFilter

A date-range chip in the same shape as a facet. Presets: Today · Yesterday · Last 7 days · Last 30 days · This month · Last month · Custom range (a two-month calendar with Apply). Set, it reads the preset ("Due: Last 7 days") or the dates from the date-time helpers ("Due: Sep 1–25"). Pass now in the same calendar-date convention as the range endpoints; it drives both the presets and the calendar's Today highlight. dateRangeForPreset(preset, now) returns the { from, to } a preset covers, for the query. The calendar loads lazily — fetched when the chip is hovered, focused or opened — so a filter row never puts the calendar engine in the page's first bundle.

PropTypeDefaultDescription
label*string—The filter's name, in sentence case ("Due date", "Created").
onValueChange*(value: DateRangeFilterValue | null) => void—Called with the new range, or null when cleared.
value*DateRangeFilterValue | null—The applied range, or null when unset (controlled).
clearLabelstring`Clear ${label}`Accessible name of the clear control.
disabledbooleanfalseDisable the filter.
localestring"en-US"The locale of the range label.
nowDatenew Date()"Today", for the presets, the custom calendar highlight and the label's year rule.
presetLabelsPartial<Record<DateRangeFilterPreset, string>>English ("Today", "Yesterday", "Last 7 days", …, "Custom range")Relabel presets — for another language.
presetsDateRangeFilterPreset[]["today", "yesterday", "last7", "last30", "thisMonth", "lastMonth", "custom"]Which presets to offer, in order. "custom" opens the calendar.
timeZonestringthe runtime's zoneThe zone the range label's days are written in (the date-time helpers' timeZone).

Data attributes and CSS variables on DateRangeFilter

AttributeValues
data-slot"date-range-filter" | "date-range-filter-clear" | "date-range-filter-content" | "date-range-filter-presets" | "date-range-filter-trigger"
data-state"set" | "unset"

FilterChip

The removable "Label: value" pill FilterRow draws for each of its chips — the Chip primitive in the row's chip shape — exported for a custom row. Give it an editor and its label and value become a button (filter-chip-trigger) that opens the editor in a popover; Escape closes it and returns focus to the chip, and the × stays its own 24px target. unset draws a pinned facet with no value yet as a dashed + Label offer.

PropTypeDefaultDescription
label*React.ReactNode—The filter's name (muted leading text).
activebooleantrueWhether the chip reads as an active selection. An active chip takes accent; an inactive chip keeps the rest fill (muted).
aibooleanfalseMade by an AI filter: a ✦ leads the chip until the filter is edited (the host clears it).
defaultEditorOpenbooleanfalseOpen the editor when the chip mounts — a filter just added from "More".
editorReact.ReactNode—Content opened from the chip's label and value, which become a button (FilterChipTrigger).
iconReact.ReactNode—Optional leading icon.
onEditorOpenChange((open: boolean) => void)—Called when the editor opens or closes.
onRemove(() => void)—Invoked when the remove (×) control is activated. An unset chip has none.
removeLabelstring—Accessible name for the remove control. Defaults to Clear <label> when label is a string.
unsetbooleanfalseA pinned facet with no value yet: a dashed + Label offer instead of a value, with no ×. Its editor opens from it.
valueReact.ReactNode—Optional value summary, rendered after label separated by a colon.

Data attributes and CSS variables on FilterChip

AttributeValues
data-slot"filter-chip" | "filter-chip-ai" | "filter-chip-trigger"
data-source"ai"
data-state"unset"

ListFilter

A descriptive applied-filter model (id, label, value, icon, editor, onRemove) for hosts that keep their filters as data and map them to chips.

PropTypeDefaultDescription
id*string—Stable identity for the chip (used as the React key and data-filter-id).
label*React.ReactNode—The filter's name in sentence case (e.g. "Status"). Rendered as the muted leading text of the chip.
onRemove*() => void—Invoked when the chip's remove (×) control is activated.
activebooleantrueWhether the chip reads as an active selection (the accent selected fill). An applied filter is a selection, so this defaults to true; set false for a presence-only chip on the rest fill.
editorReact.ReactNode—Edit the filter in place: the chip's label and value become a button that opens this content in a popover (a facet list, a date range, a number range). The remove control stays its own target.
iconReact.ReactNode—Optional leading icon — a single lucide-react / @vegastack/design/icons element.
valueReact.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.

Accessibility

  • The chips are a group named "Filters". Each chip's × is named "Clear" and the filter's label.
  • An AI chip's ✦ is labelled "Added by AI", so the mark is read with the chip.
  • The AI note is a polite status: it is announced once after a run. Undo and the dismiss × are buttons.
  • Clear hands focus back to the row's first control once it unmounts.
  • A ListFacet, PeopleFacet or DateRangeFilter is a button named by what it reads ("Status", "Status: Open"); its × is named "Clear" and the label, and Escape closes its popover and returns focus to it.
KeyAction
TabMove through + Filter, chips, Clear
Enter / SpaceOpen a chip's editor, remove, clear
ContractStates tested
Behaviourdefault, set, ai, active, disabled, facet, editing
Accessibilitylabeled, browser-accessibility-test
Visualdefault, hover

Do / Don't

Do
Keep the order fixed, show unset pinned facets as dashed offers, and remove exactly the AI's chips on Undo.
Don't
Hide a default filter behind a count, add a separate preset chip type, or guess at words the AI could not match.

On this page