Filter Builder
The stateful nested and/or filter builder — host-injected field grammar, per-type value editors, depth and condition caps, FilterChip summary.
- Status
- Since
0.4.0- Accessibility pattern
- nested fieldset/legend groups
Last updated
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-managedThe 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
| Behaviour | Where it lives |
|---|---|
| The field grammar | Host, via vocabulary — never baked in |
| Value editors beyond text | Host, via editors (date pickers, actor pickers, …) |
| Serialisation / URL state | Host — the tree is plain data |
| Running the filter | Host — the output describes conditions; it never executes them |
| AI-suggested filters | Host — 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.
Depth and condition caps
Both caps reached: the add affordances disable with a readable reason.
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
onValueChange* | (value: Extract<FilterNode<V>, { type: "group"; }>) => void | — | Fired 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-label | string | "Filter conditions" | Accessible name for the builder. |
className | string | — | Extra classes for the root element. |
disabled | boolean | false | Disable every control. |
editors | Record<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. |
maxConditions | number | 25 | Maximum total conditions across the tree. The add affordances disable at the cap. |
maxDepth | number | 3 | Maximum 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. |
readOnly | boolean | false | Render 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. |
ref | React.Ref<HTMLDivElement> | — | Ref forwarded to the root element. |
Data attributes and CSS variables on FilterBuilder
| Attribute | Values |
|---|---|
data-depth | mirrors a prop or state value |
data-disabled | "" |
data-invalid | "" |
data-op | mirrors 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
| Prop | Type | Default | Description |
|---|---|---|---|
key* | string | — | Stable field key ("stage", "amount"). |
label* | string | — | Visible label ("Stage", "Amount"). |
operators* | readonly FilterOperator[] | — | Operators this field accepts, in menu order. |
type* | string | — | Editor 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
| Prop | Type | Default | Description |
|---|---|---|---|
aria-label* | string | — | Accessible name for the editor control. |
field* | FilterField<V> | — | The condition's field definition. |
onValueChange* | (value: V | undefined) => void | — | Commit a new value. |
operator* | string | — | The condition's current operator. |
value* | V | undefined | — | Current value (may be undefined). |
aria-describedby | string | — | Points at the condition's "Value required" message when invalid. |
aria-invalid | boolean | — | Set when the condition is missing a required value. |
disabled | boolean | false | Whether the surrounding builder is disabled. |
id | string | — | Stable id for the editor control (used by the condition's error wiring). |
Accessibility
- The tree is nested
<fieldset>/<legend>structure — deliberately notrole="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.
| Key | Action |
|---|---|
| Tab | Move through field, operator, value, remove. |
| Enter / Space | Open the focused picker or activate a button. |
| Contract | States tested |
|---|---|
| Behaviour | empty, conditions, nested-groups, depth-cap, condition-cap, invalid-condition, disabled, read-only-summary |
| Accessibility | keyboard, labeled, semantic-html |
| Visual | default, invalid, disabled, dark |