A lightweight, headless React query builder with drag-and-drop support. Build complex filter UIs with any design system — zero styling opinions.
Most query builders ship with opinionated styles or are tightly coupled to specific UI libraries. This library provides:
- Complete UI freedom — Use MUI, Chakra, Ant Design, Tailwind, or vanilla HTML
- Inversion of control — You own the markup, we handle the logic
- Type safety — Full TypeScript inference for queries, operators, and fields
- Lightweight — ~18KB minified + gzipped, including drag-and-drop support
| Feature | Description |
|---|---|
| Headless | No styles, no markup — bring your own components |
| Compound Components | Clean <QueryBuilder.Builder> composition API |
| Render Props | Full control via renderRule and renderGroup |
| Drag & Drop | Optional reordering via dnd-kit integration |
| Immutable Updates | Predictable state with structural sharing |
| Type Inference | Operators auto-filter based on field type |
| Nested Groups | Recursive AND/OR groups with maxDepth control |
| Lock Protection | Prevent modification of locked rules/groups |
| Slot Actions | Pre-wired handlers for add, remove, clone, lock |
npm install react-querybuilder-liteyarn add react-querybuilder-litepnpm add react-querybuilder-litePeer Dependencies: React 16.8+
import { useState } from 'react';
import { QueryBuilder, type Query } from 'react-querybuilder-lite';
const fields = [
{ label: 'Name', value: 'name', type: 'string' },
{ label: 'Age', value: 'age', type: 'number' },
];
const initialQuery: Query = {
id: 'root',
combinator: 'and',
rules: [],
};
function App() {
const [query, setQuery] = useState<Query>(initialQuery);
return (
<QueryBuilder value={query} onChange={setQuery} maxDepth={3}>
<QueryBuilder.Builder
fields={fields}
renderRule={({ rule, fields, operators, onChange, slots }) => (
<div className="rule">
<select
value={rule.field}
onChange={(e) => onChange({ field: e.target.value })}
>
<option value="">Select field</option>
{fields.map((f) => (
<option key={f.value} value={f.value}>{f.label}</option>
))}
</select>
<select
value={rule.operator}
onChange={(e) => onChange({ operator: e.target.value })}
>
{operators.map((op) => (
<option key={op.value} value={op.value}>{op.name}</option>
))}
</select>
<input
value={rule.value ?? ''}
onChange={(e) => onChange({ value: e.target.value })}
/>
<button onClick={slots.onRemove}>Remove</button>
</div>
)}
renderGroup={({ group, children, onChange, slots }) => (
<div className="group">
<select
value={group.combinator}
onChange={(e) => onChange({ combinator: e.target.value })}
>
<option value="and">AND</option>
<option value="or">OR</option>
</select>
<button onClick={slots.onAddRule}>+ Rule</button>
<button onClick={slots.onAddGroup}>+ Group</button>
<div className="rules">{children}</div>
</div>
)}
/>
</QueryBuilder>
);
}Use QueryBuilder.BuilderWithDnD to enable drag-and-drop reordering.
import { QueryBuilder, type Query } from 'react-querybuilder-lite';
<QueryBuilder value={query} onChange={setQuery}>
<QueryBuilder.BuilderWithDnD
fields={fields}
renderRule={({ rule, fields, operators, onChange, slots }) => (
<div className="rule">
{/* Drag handle - spread slots.dragHandles on any element */}
<span className="drag-handle" {...slots.dragHandles}>⠿</span>
<select value={rule.field} onChange={(e) => onChange({ field: e.target.value })}>
{fields.map((f) => <option key={f.value} value={f.value}>{f.label}</option>)}
</select>
{/* ... rest of your UI */}
</div>
)}
renderGroup={({ group, children, onChange, slots }) => (
<div className="group">
<span className="drag-handle" {...slots.dragHandles}>⠿</span>
<select value={group.combinator} onChange={(e) => onChange({ combinator: e.target.value })}>
<option value="and">AND</option>
<option value="or">OR</option>
</select>
<button onClick={slots.onAddRule}>+ Rule</button>
<button onClick={slots.onAddGroup}>+ Group</button>
{children}
</div>
)}
/>
</QueryBuilder>Root component that provides state management context.
| Prop | Type | Required | Description |
|---|---|---|---|
value |
Query |
Yes | The query state |
onChange |
(query: Query) => void |
Yes | Called when query changes |
maxDepth |
number |
No | Maximum nesting depth. 1 = no nesting, 2 = one level, etc. |
children |
ReactNode |
Yes | Must contain Builder or BuilderWithDnD |
Renders the query tree without drag-and-drop.
| Prop | Type | Required | Description |
|---|---|---|---|
fields |
Field[] |
Yes | Available fields for rules |
renderRule |
(props: RuleRenderProps) => ReactNode |
Yes | Render function for rules |
renderGroup |
(props: GroupRenderProps) => ReactNode |
Yes | Render function for groups |
operatorsByFieldType |
Record<FieldType, Operator[]> |
No | Custom operator mapping |
Same props as Builder, plus optional drag preview customization.
| Prop | Type | Required | Description |
|---|---|---|---|
renderDragPreview |
(props: DragPreviewProps) => ReactNode |
No | Custom drag overlay |
interface RuleRenderProps {
rule: Rule; // Current rule data
path: number[]; // Position in tree (e.g., [0, 1])
depth: number; // Nesting level
fields: Field[]; // Available fields
operators: Operator[]; // Operators for selected field type
selectedField?: Field; // Currently selected field
selectedOperator?: Operator; // Currently selected operator
slots: {
onRemove: () => void; // Remove this rule
onClone: () => void; // Duplicate this rule
onToggleLock: () => void; // Toggle lock state
dragHandles: DragHandleType; // Spread on drag handle element
};
onChange: (updates: Partial<Rule>) => void; // Update rule
}interface GroupRenderProps {
group: RuleGroup; // Current group data
path: number[]; // Position in tree
depth: number; // Nesting level
children: ReactNode; // Rendered child rules/groups
slots: {
onAddRule: () => void; // Add rule to this group
onAddGroup: () => void; // Add nested group
onRemove: () => void; // Remove this group
onClone: () => void; // Duplicate this group
onToggleLock: () => void; // Toggle lock state
dragHandles: DragHandleType; // Spread on drag handle element
};
onChange: (updates: Partial<RuleGroup>) => void; // Update group
}| Term | Description |
|---|---|
| Field (or Column) | The data attribute you want to filter on. For example, "First Name", "Age", "Created Date" are fields. |
| Operator | The comparison operation like "equals", "contains", "greater than". |
| Field Type | Category of the field that determines available operators. Default types: string, number, boolean, date. You can define custom types. |
| Combinator | Logical operator to combine rules: AND or OR. |
// The root query structure
type Query = RuleGroup;
interface RuleGroup {
id: string;
combinator: 'and' | 'or';
rules: Array<Rule | RuleGroup>;
isLocked?: boolean;
}
interface Rule {
id: string;
field: string;
operator: OperatorKey;
value?: Value;
isLocked?: boolean;
}
interface Field {
label: string;
value: string;
type: string; // 'string' | 'number' | 'boolean' | 'date' or any custom type
}Built-in operators organized by type:
| Type | Operators |
|---|---|
| Unary | is_empty, is_not_empty, is_true, is_false |
| Binary | equal, not_equal, less, less_or_equal, greater, greater_or_equal, contains, starts_with, ends_with |
| Range | between, not_between |
| List | in, not_in |
Operators are automatically filtered by field type:
| Field Type | Available Operators |
|---|---|
string |
is_empty, is_not_empty, equal, not_equal, contains, starts_with, ends_with, in, not_in |
number |
is_empty, is_not_empty, equal, not_equal, less, less_or_equal, greater, greater_or_equal, between, not_between, in, not_in |
boolean |
is_empty, is_not_empty, is_true, is_false |
date |
is_empty, is_not_empty, equal, not_equal, less, greater, between, not_between, in, not_in |
You're not limited to the default field types. Define your own types with custom operators:
import { QueryBuilder, type Query, type Operator } from 'react-querybuilder-lite';
// Define fields with custom types
const fields = [
{ label: 'Name', value: 'name', type: 'string' },
{ label: 'Email', value: 'email', type: 'email' }, // Custom type
{ label: 'Created', value: 'createdAt', type: 'datetime' }, // Custom type
{ label: 'Price', value: 'price', type: 'currency' }, // Custom type
];
// Provide operators for your custom types
const operatorsByFieldType: Record<string, Operator[]> = {
string: [
{ name: 'Equals', value: 'equal', type: 'binary' },
{ name: 'Contains', value: 'contains', type: 'binary' },
],
email: [
{ name: 'Is', value: 'equal', type: 'binary' },
{ name: 'Contains', value: 'contains', type: 'binary' },
{ name: 'Ends With', value: 'ends_with', type: 'binary' },
],
datetime: [
{ name: 'Before', value: 'less', type: 'binary' },
{ name: 'After', value: 'greater', type: 'binary' },
{ name: 'Between', value: 'between', type: 'range' },
],
currency: [
{ name: 'Equals', value: 'equal', type: 'binary' },
{ name: 'Greater Than', value: 'greater', type: 'binary' },
{ name: 'Less Than', value: 'less', type: 'binary' },
{ name: 'Between', value: 'between', type: 'range' },
],
};
<QueryBuilder value={query} onChange={setQuery}>
<QueryBuilder.Builder
fields={fields}
operatorsByFieldType={operatorsByFieldType}
renderRule={...}
renderGroup={...}
/>
</QueryBuilder>Full internationalization support. You control all user-facing text:
fields— Translated field labelsoperatorsByFieldType— Translated operator namesrenderRule/renderGroup— Your components, your language. Full control over buttons, placeholders, and combinatorsdragDropAccessibility— Translated screen reader announcements
See the Localization story in Storybook → for examples in Spanish, Japanese, and French. Need another language? Easy to configure refer to the comprehensive documentation in the story.
Interactive examples showcasing all components with different configurations.
| Decision | Rationale |
|---|---|
| Headless architecture | Maximum flexibility, framework agnostic |
| Compound components | Implicit state sharing without prop drilling |
| Path-based operations | O(depth) updates with structural sharing |
| Render props over slots | Full control vs. limited customization |
| Cascading lock state | UX: locked parent = locked children |
| Optional DnD entry point | Respects bundle budgets |
Contributions are welcome! Please open an issue or submit a PR.
MIT