Skip to content
Dice UI

Query Builder

API

A composable query builder for nested filter rules and groups, with parameterized SQL output.

Installation

pnpm dlx shadcn@latest add @diceui/query-builder

Layout

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`,
  }}
/>
LabelDefaultUsed 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
operatorLabelgetQueryOperatorLabelOperator 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 QueryBuilderGroup to get the group's rules.
  • Pass a function to QueryBuilderRuleList to wrap each rule and nested group in a sortable item.
  • Use moveQueryNode to 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

TypeOperators
textiLike, notILike, eq, ne, isEmpty, isNotEmpty
numbereq, ne, lt, lte, gt, gte, isBetween, isEmpty, isNotEmpty
dateeq, ne, lt, gt, lte, gte, isBetween, isEmpty, isNotEmpty
booleaneq, ne
selecteq, ne, inArray, notInArray, isEmpty, isNotEmpty
multiSelectinArray, 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 AttributeValue
[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 AttributeValue
[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 AttributeValue
[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 AttributeValue
[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.

Prop

Type

Data AttributeValue
[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 AttributeValue
[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 AttributeValue
[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 AttributeValue
[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 isEmpty and isNotEmpty. 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

KeyDescription
TabMoves focus to the next control.
EnterSpaceOpens the focused select or picker, or activates the focused button.
ArrowUpArrowDownMoves between options in an open select or picker.
EscapeCloses the open select or picker.

On this page