Query Builder
A composable query builder for nested filter rules and groups, with parameterized SQL output.
Installation
pnpm dlx shadcn@latest add @diceui/query-builderLayout
Compose the parts like this. QueryBuilderRuleList renders its children once per rule, and nested groups reuse the root group's layout. Wrap a rule's controls in a ButtonGroup to join them.
import { ButtonGroup } from "@/components/ui/button-group";
import {
QueryBuilder,
QueryBuilderAddGroup,
QueryBuilderAddRule,
QueryBuilderConnector,
QueryBuilderField,
QueryBuilderGroup,
QueryBuilderGroupContent,
QueryBuilderGroupFooter,
QueryBuilderGroupHeader,
QueryBuilderGroupTitle,
QueryBuilderOperator,
QueryBuilderRemove,
QueryBuilderRule,
QueryBuilderRuleList,
QueryBuilderValue,
} from "@/components/ui/query-builder";
return (
<QueryBuilder fields={fields}>
<QueryBuilderGroup>
<QueryBuilderConnector />
<QueryBuilderGroupContent>
<QueryBuilderGroupHeader>
<QueryBuilderGroupTitle />
<QueryBuilderAddRule />
<QueryBuilderAddGroup />
<QueryBuilderRemove />
</QueryBuilderGroupHeader>
<QueryBuilderRuleList>
<QueryBuilderRule>
<QueryBuilderConnector />
<ButtonGroup>
<QueryBuilderField />
<QueryBuilderOperator />
<QueryBuilderValue />
<QueryBuilderRemove />
</ButtonGroup>
</QueryBuilderRule>
</QueryBuilderRuleList>
<QueryBuilderGroupFooter>
<QueryBuilderAddRule />
<QueryBuilderAddGroup />
</QueryBuilderGroupFooter>
</QueryBuilderGroupContent>
</QueryBuilderGroup>
</QueryBuilder>
);The header, content and footer have data-depth, so you can show the header only on nested groups and the footer only on the root:
<QueryBuilderGroupHeader className="data-[depth=0]:hidden" />
<QueryBuilderGroupFooter className="not-data-[depth=0]:hidden" />Labels
Pass labels to reword or translate the built-in text, including text inside the pickers that children can't reach. Labels you leave out, or set to undefined, keep their default. Labels that include a value, like selected, are functions.
<QueryBuilder
fields={fields}
labels={{
where: "Où",
and: "Et",
or: "Ou",
addRule: "Ajouter une règle",
selected: (count) => `${count} sélectionnés`,
}}
/>| Label | Default | Used for |
|---|---|---|
where | "Where" | Connector before the first rule |
and | "And" | Connector and combinator option |
or | "Or" | Connector and combinator option |
all | "All" | QueryBuilderCombinator option |
any | "Any" | QueryBuilderCombinator option |
not | "Not" | QueryBuilderNegate text |
combinator | "Combinator" | Combinator select's accessible name |
matchAll | "All of the following are true…" | Group title |
matchAny | "Any of the following are true…" | Group title |
matchNotAll | "Not all of the following are true…" | Negated group title |
matchNone | "None of the following are true…" | Negated group title |
field | "Field" | Field picker's accessible name and placeholder |
searchFields | "Search fields" | Field search placeholder |
noFields | "No fields found." | Field picker empty state |
operator | "Operator" | Operator picker's accessible name |
operatorLabel | getQueryOperatorLabel | Operator text, like "contains" or "is before" |
searchOperators | "Search operators" | Operator search placeholder |
noOperators | "No operators found." | Operator picker empty state |
enterValue | "Enter a value" | Value input placeholder |
selectValue | "Select a value" | Value picker placeholder |
selectValues | "Select values" | Value picker placeholder for several values |
searchValues | (field) => `Search ${field.toLowerCase()}` | Value search placeholder |
selected | (count) => `${count} selected` | Value picker summary |
noValue | "no value" | Value picker's accessible name when empty |
noValues | "No results." | Value picker empty state |
between | "and" | Text between range inputs |
from | "from" | First range input's accessible name |
to | "to" | Second range input's accessible name |
true | "True" | Boolean option |
false | "False" | Boolean option |
addRule | "Add rule" | QueryBuilderAddRule text |
addGroup | "Add group" | QueryBuilderAddGroup text |
removeRule | "Remove rule" | QueryBuilderRemove accessible name in a rule |
removeGroup | "Remove group" | QueryBuilderRemove accessible name in a group |
Operator text comes from operatorLabel, which takes the operator and the field type, so a date field can say "is before" instead of "is less than". With an i18n library, pass something like (operator) => t(`operators.${operator}`).
Examples
SQL
formatQuery turns the query into a parameterized WHERE clause. Values go into params and columns come from fields, so the output is safe to pass to your database driver.
Filtering data
Filter rows in the browser with matchesQuery.
Sortable
Drag rules and nested groups with Sortable.
- Pass a function to
QueryBuilderGroupto get the group's rules. - Pass a function to
QueryBuilderRuleListto wrap each rule and nested group in a sortable item. - Use
moveQueryNodeto move an item into another group.
Command filter
A Linear-style filter bar built with Command Stack. getQuerySelection and setQuerySelection keep the menu and the query in sync.
Query
A query is a tree of groups and rules. Groups combine their rules with and or or, and can be negated with not.
const query: QueryGroup = {
id: "root",
combinator: "and",
rules: [
{ id: "1", field: "status", operator: "eq", value: "todo" },
{
id: "2",
combinator: "or",
rules: [
{ id: "3", field: "estimate", operator: "gte", value: "5" },
{ id: "4", field: "title", operator: "iLike", value: "bug" },
],
},
],
};Values are strings, or string arrays for isBetween, inArray and notInArray, so a query serializes to JSON or the URL. Dates use YYYY-MM-DD and match the whole day. Rules that aren't filled in are skipped.
Fields and operators
| Type | Operators |
|---|---|
text | iLike, notILike, eq, ne, isEmpty, isNotEmpty |
number | eq, ne, lt, lte, gt, gte, isBetween, isEmpty, isNotEmpty |
date | eq, ne, lt, gt, lte, gte, isBetween, isEmpty, isNotEmpty |
boolean | eq, ne |
select | eq, ne, inArray, notInArray, isEmpty, isNotEmpty |
multiSelect | inArray, notInArray, isEmpty, isNotEmpty |
Pass operators on a field to narrow or reorder its operators. Options can take an icon, shown next to the label.
On the server
import { formatQuery } from "@/lib/query-builder";
const { sql, params } = formatQuery(query, fields, { dialect: "postgres" });
const result = await db.query(
`SELECT * FROM tasks${sql ? ` WHERE ${sql}` : ""}`,
params,
);dialect is postgres (the default, with $1 placeholders), mysql or sqlite (with ? placeholders). Set column on a field to map it to another column, like created_at.
On the client
matchesQuery checks a row against the same query, with the same rules as the SQL output.
import { matchesQuery } from "@/lib/query-builder";
const rows = tasks.filter((task) => matchesQuery(task, query, fields));API Reference
QueryBuilder
The root component. Holds the query and the fields it can use. disabled disables every part, and each control also takes its own disabled.
Prop
Type
| Data Attribute | Value |
|---|---|
[data-disabled] | Present when disabled. |
QueryBuilderGroup
A group of rules. Used for the root group and repeated for every nested group. Its children can be a function that takes the group.
Prop
Type
| Data Attribute | Value |
|---|---|
[data-depth] | How deep the group is nested, starting at 0 for the root group. |
[data-combinator] | ""and"" | ""or"" |
[data-not] | Present when the group is negated. |
QueryBuilderGroupContent
The group's rules and actions. Nested groups draw it as a box.
Prop
Type
| Data Attribute | Value |
|---|---|
[data-depth] | How deep the group is nested, starting at 0 for the root group. |
QueryBuilderGroupHeader
Holds the group's title and actions.
Prop
Type
| Data Attribute | Value |
|---|---|
[data-depth] | How deep the group is nested, starting at 0 for the root group. |
QueryBuilderGroupTitle
Describes the group based on its combinator, like "Any of the following are true…", or "None of the following are true…" when negated. Pass children to change the text.
Prop
Type
QueryBuilderGroupFooter
Holds actions below the group's rules.
| Data Attribute | Value |
|---|---|
[data-depth] | How deep the group is nested, starting at 0 for the root group. |
QueryBuilderRuleList
Renders the group's rules and nested groups. Pass the layout for each rule as children, or a function that takes each rule or nested group. In the function, render <QueryBuilderGroup /> for a nested group to reuse the root group's layout.
Prop
Type
QueryBuilderRule
A single rule.
Prop
Type
| Data Attribute | Value |
|---|---|
[data-operator] | The rule's operator. |
QueryBuilderConnector
Shows how a rule or nested group joins the ones before it:
- "Where" before the first.
- A combinator select before the second.
- "And" or "Or" after that.
Renders nothing outside QueryBuilderRuleList. The select's accessible name is the combinator label.
Prop
Type
| Data Attribute | Value |
|---|---|
[data-type] | ""rule"" | ""group"" |
QueryBuilderField
A searchable Faceted picker for the rule's field. Accepts the props of Button.
- Switching to a field of the same type that supports the current operator keeps the operator and value, minus select values the new field doesn't have. If one value is left, "is any of" becomes "is" and "is none of" becomes "is not".
- Otherwise the rule resets to the new field's first operator.
To hide the chevron in the field, operator and value pickers, target data-slot="query-builder-select-icon", like **:data-[slot=query-builder-select-icon]:hidden on a wrapper.
Prop
Type
QueryBuilderOperator
A searchable picker for the rule's operator. Accepts the props of Button. Changing the operator keeps the value. Switching between "is" and "is any of" turns a single value into a list, or a list into its first value.
Prop
Type
QueryBuilderCombinator
A select for the group's combinator, labeled "All" and "Any", for headers like "Match [All] of the following". Accepts the props of SelectTrigger. To show the combinator between rules, use QueryBuilderConnector.
Prop
Type
QueryBuilderNegate
A toggle that negates the group, so it matches when its rules don't. Accepts the props of Button and sets aria-pressed while on. The SQL example negates its nested group, which outputs NOT (...).
Prop
Type
| Data Attribute | Value |
|---|---|
[data-state] | ""on"" | ""off"" |
QueryBuilderValue
The rule's value, based on the field type and operator:
- An input for text, number and date fields.
- Two inputs for
isBetween. - A searchable Faceted picker for select and boolean fields.
- An empty, read-only input for
isEmptyandisNotEmpty. It keeps the rule's width and is hidden from screen readers.
It renders the control without a wrapper, so it fits in a ButtonGroup, and passes HTML attributes like id to it. For a range, id goes on the first input, aria-label or aria-labelledby names both, like "Price from" and "Price to", and other attributes go on both.
A select field takes several values when its operators include both forms, like Linear's filters. Picking a second value switches eq to inArray and ne to notInArray, and removing it switches back. setQueryRuleValues does the same outside the picker.
The trigger shows a badge per value, then +N for the ones that don't fit, and the menu has a Clear action. Set display="summary" to show the value's label, or a count like "2 selected", instead.
Prop
Type
QueryBuilderAddRule
Adds a rule to the group. Set field to the value of the field the rule starts with, or leave it out to use the first field.
Prop
Type
QueryBuilderAddGroup
Adds a nested group with one rule, using field like QueryBuilderAddRule. The new group uses the opposite of its parent's combinator. Hidden once the group is at maxDepth.
Prop
Type
QueryBuilderRemove
Removes the rule it's in, or the group when used outside a rule. Hidden in the root group.
Prop
Type
Accessibility
The root group renders with role="group", and its rules and nested groups as a list, so a nested group reads as a list item holding its own list. The combinator follows the Select keyboard pattern, and the field, operator and value pickers follow Faceted.
The field, operator and value pickers include their selection in their accessible name, like "Status: Todo, In progress", so values hidden behind +N are still announced, and the value picker lists them in its title. Option icons are decorative, so give options icons that differ by shape, not only by color.
Keyboard Interactions
| Key | Description |
|---|---|
| Tab | Moves focus to the next control. |
| EnterSpace | Opens the focused select or picker, or activates the focused button. |
| ArrowUpArrowDown | Moves between options in an open select or picker. |
| Escape | Closes the open select or picker. |