Query Builder
A tree list of field rows with always-visible operator/value rules, nested and/or groups, a live natural-language preview and Submit/Reset — zero external dependencies.
Installation
Usage
QueryBuilder renders the query as a flat tree list. Every field appears exactly once per group as a row — label plus a kind badge — with the add (+) button at the end of the row. Its conditions render directly beneath it as child rows joined by tree guides (├ for every child, └ for the last), each child being an operator dropdown plus a value editor with the remove button at the end — the right edge is always the action area. A field with no conditions renders nothing extra. The layout is fully recursive: the root group and every added group share the same first row — collapse chevron, AND / OR segment and the + Group button — above the tree list, which scrolls inside a fixed-height frame. Below it, a read-only natural-language preview panel shows the query, and centered Submit / Reset buttons sit at the very bottom.
No conditions yet
"use client";import { QueryBuilder } from "@/ui";const fields = [ { name: "name", label: "Name", operators: ["=", "!=", "contains"], valueKind: "text" as const }, { name: "price", label: "Price", valueKind: "number" as const }, { name: "created", label: "Created", valueKind: "date" as const }, { name: "status", label: "Status", valueKind: "select" as const, options: [ { label: "Active", value: "active" }, { label: "Inactive", value: "inactive" }, { label: "Archived", value: "archived" }, ], }, { name: "region", label: "Region", valueKind: "select" as const, options: [ { label: "North America", value: "na" }, { label: "Europe", value: "eu" }, { label: "Asia Pacific", value: "apac" }, ], },];export default function Demo() { return ( <div className="w-full max-w-3xl"> <QueryBuilder fields={fields} /> </div> );}- text / number / date — press the field row's
+to append a condition row; each row is an operator dropdown plus a value input, and the trailing trash button removes it. - Group — a nested root: exactly the same layout as the root group — same first row, same tree list — so nesting reads as one recursive pattern.
Enum fields
valueKind: "select" fields render their options always visible as child rows (tree-guided like any other child): a select-all row followed by one checkbox row per option. Checking and unchecking options directly creates and removes the condition; the checked options merge into a single IN condition (never split into ORed equals). Only forward IN is offered — no NOT IN; to invert a selection, check everything and uncheck what you don't need.
Natural-language preview
The preview is built from field labels and reads for humans: sibling rules are joined by the group combinator (and / or), nested groups are always parenthesized, and values are formatted per kind — numbers raw, text and dates single-quoted (embedded quotes doubled), and select IN renders as ('vip', 'beta'). An empty tree previews as "No conditions yet".
Operators render as English phrases by default, RQB-style: Name is 'fung', Price is greater than 30, Status is one of ('vip', 'beta').
Fields are label/value pairs: name is what gets written into the query, label is what humans read — field rows and the preview always render the label, so the same query tree localizes freely.
Operators are localized through a static property — it feeds both the operator dropdowns and the preview:
import { QueryBuilder } from "@/ui";
QueryBuilder.operatorLabels = {
"=": "为",
"!=": "不为",
">": "大于",
">=": "大于等于",
"<": "小于",
"<=": "小于等于",
contains: "包含",
in: "属于",
};Unknown operators fall back to their raw string, so custom operators keep working without configuration.
Submit and Reset
The tree is a draft: edits update the live preview only. Submit emits the query tree through onSubmit / onQueryChange as a structured QueryGroup object ({ combinator: "and" | "or", rules: [{ field, operator, value } | QueryGroup] }). Reset restores the draft to defaultValue (or an empty tree).
API Reference
QueryBuilder
| Prop | Type | Default | Description |
|---|---|---|---|
fields | QueryFieldConfig[] | - | Available fields; each renders as one unique field row. Required |
defaultValue | QueryGroup | empty and tree | Initial query; also what Reset restores |
onSubmit | (query: QueryGroup) => void | - | Fired by the Submit button |
onQueryChange | (query: QueryGroup) => void | - | Alias of onSubmit |
onReset | () => void | - | Fired by the Reset button |
showPreview | boolean | true | Show the read-only natural-language preview panel |
maxDepth | number | 2 | Maximum group nesting depth |
disabled | boolean | false | Disable the builder |
className | ClassNameValue | - | Custom classes, applied to the root |
QueryFieldConfig
| Field | Type | Default | Description |
|---|---|---|---|
name | string | - | Field identifier written into the query. Required |
label | string | name | Field label shown on its field row and in the preview |
operators | string[] | per valueKind | Operators offered for sub-conditions |
valueKind | "text" | "number" | "date" | "select" | "text" | Value editor type |
options | { label, value }[] | - | Options when valueKind: "select" |
Security: the query leaves as JSON, never as SQL
onSubmit / onQueryChange hand you the structured query object — never a SQL string. The natural-language preview is display-only, rendered in the browser for human reading. Do not send any preview string to your backend for execution: client-generated SQL cannot be trusted or safely validated, and executing it would bypass parameterization as well as any row-level scoping.
The safe backend flow is: receive the JSON object → validate it against a whitelist (allowed fields, allowed operators per field, value types and ranges) → convert it to parameterized SQL server-side, binding every value as a query parameter. The frontend's job is the editing experience; dialect correctness and safety both belong to the backend.
This component has zero external dependencies — dialect conversion (SQL, MongoDB, CEL, …) is implemented server-side from the submitted JSON.
Statics
| Property | Type | Description |
|---|---|---|
QueryBuilder.operatorLabels | Record<string, string> | Operator display text for the dropdowns and the natural-language preview. Defaults are English natural-language phrases (= → "is", > → "is greater than", in → "is one of", …); assign a partial map to override (e.g. Chinese) — missing keys fall back to the raw operator |