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 Builder

The stateful nested and/or filter builder — host-injected field grammar, per-type value editors, depth and condition caps, FilterChip summary.

Status
stable
Since
0.4.0
Accessibility pattern
nested fieldset/legend groups

Last updated

Filter conditions
Condition group (Any condition matches)

Install

Add Filter Builder from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.

pnpm dlx shadcn@latest add @vegastack/filter-bar-managed

The same command installs the registry items it composes: @vegastack/button, @vegastack/icon-button, @vegastack/input, @vegastack/filter-bar, @vegastack/select.

Usage

import {
  FilterBuilder,
  type FilterField,
  type FilterNode,
} from "@/components/ui/filter-bar-managed";

const VOCABULARY: FilterField<string>[] = [
  {
    key: "stage",
    label: "Stage",
    type: "text",
    operators: [{ value: "is", label: "is" }],
  },
];

const [tree, setTree] = useState<
  Extract<FilterNode<string>, { type: "group" }>
>({
  type: "group",
  op: "and",
  children: [],
});

<FilterBuilder vocabulary={VOCABULARY} value={tree} onValueChange={setTree} />;

This is the stateful sibling FilterBar's docs promised. The grammar is host-injected: the component owns the tree shape (FilterNode — nested and/or groups and condition rows) and its editing surface; your app supplies the vocabulary (which fields exist, which operators each accepts, which editor type renders the value) and the editors registry. The component never validates a field's semantics and never serialises — the moment it did either, it would have adopted one app's AST.

Scope

BehaviourWhere it lives
The field grammarHost, via vocabulary — never baked in
Value editors beyond textHost, via editors (date pickers, actor pickers, …)
Serialisation / URL stateHost — the tree is plain data
Running the filterHost — the output describes conditions; it never executes them
AI-suggested filtersHost — app-coupled per the G7 split

Examples

Chip summary

readOnly collapses the builder to a flat FilterChip row (the and/or grouping is not visualised in the summary — reopen the builder to see structure). Chips stay removable — removing one prunes that condition from the tree — unless the builder is also disabled, which makes the summary inert.

Stageis QualifiedAmountgreater than 10000Owneris empty

Depth and condition caps

Filter conditions
A filter can hold 3 conditions at most

Both caps reached: the add affordances disable with a readable reason.

API Reference

PropTypeDefaultDescription
onValueChange*(value: Extract<FilterNode<V>, { type: "group"; }>) => voidFired with the next tree on every edit.
value*{ type: "group"; op: "and" | "or"; children: FilterNode<V>[]; }The controlled filter tree. Must be a group node at the root.
vocabulary*readonly FilterField<V>[]The host's field grammar. Order is menu order.
aria-labelstring"Filter conditions"Accessible name for the builder.
classNamestringExtra classes for the root element.
disabledbooleanfalseDisable every control.
editorsRecord<string, React.ComponentType<FilterValueEditorProps<V>>>Per-type value editors, keyed by FilterField.type. Types without an entry fall back to a STRING-VALUED text Input — when V is not string, every field type needs an entry here. Editors are host code — a date field should render the host's date picker, not a text box.
maxConditionsnumber25Maximum total conditions across the tree. The add affordances disable at the cap.
maxDepthnumber3Maximum group nesting depth (the root group is depth 1). The add-group affordance disables at the cap and the reason renders as visible text beside it.
readOnlybooleanfalseRender the collapsed chip summary (via FilterChip) instead of the full editing surface. Chips stay removable — removing one prunes that condition from the tree — unless disabled is also set.
refReact.Ref<HTMLDivElement>Ref forwarded to the root element.

Data attributes and CSS variables on FilterBuilder

AttributeValues
data-depthmirrors a prop or state value
data-disabled""
data-invalid""
data-opmirrors a prop or state value
data-read-only""
data-slot"filter-builder" | "filter-builder-cap-reason" | "filter-builder-condition" | "filter-builder-condition-error" | "filter-builder-group"

Field definition

PropTypeDefaultDescription
key*stringStable field key ("stage", "amount").
label*stringVisible label ("Stage", "Amount").
operators*readonly FilterOperator[]Operators this field accepts, in menu order.
type*stringEditor type for the value — keys the editors registry. Opaque to the component: any string works as long as an editor exists for it (a built-in text editor is the fallback).
formatValue((value: V) => string)Format a value for the read-only chip summary.

Value editor contract

PropTypeDefaultDescription
aria-label*stringAccessible name for the editor control.
field*FilterField<V>The condition's field definition.
onValueChange*(value: V | undefined) => voidCommit a new value.
operator*stringThe condition's current operator.
value*V | undefinedCurrent value (may be undefined).
aria-describedbystringPoints at the condition's "Value required" message when invalid.
aria-invalidbooleanSet when the condition is missing a required value.
disabledbooleanfalseWhether the surrounding builder is disabled.
idstringStable id for the editor control (used by the condition's error wiring).

Accessibility

  • The tree is nested <fieldset>/<legend> structure — deliberately not role="tree", where editing controls inside tree items are a known screen-reader trap. Every control stays an ordinary labelled form control.
  • Removing a condition moves focus to the next condition in the same group, or to the group's add-condition button when it was the last — never back to the top of the page.
  • A value-requiring condition without a value is flagged with visible text ("Value required") and data-invalid, never colour alone.
  • At the depth or condition cap the add affordances disable and the reason renders as visible text beside them.
KeyAction
TabMove through field, operator, value, remove.
Enter / SpaceOpen the focused picker or activate a button.
ContractStates tested
Behaviourempty, conditions, nested-groups, depth-cap, condition-cap, invalid-condition, disabled, read-only-summary
Accessibilitykeyboard, labeled, semantic-html
Visualdefault, invalid, disabled, dark

Do / Don't

Do
Inject the grammar: keep field keys, operators, and value editors in your app and pass them as vocabulary.
Don't
Ask the component to validate field semantics or serialise the tree — that adopts one app's AST into the design system.

On this page