A lightweight, headless React query builder with drag-and-drop and Typescript support. Build complex filters visually.
npm install react-querybuilder-lite
!TypeScript
!React



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 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 |
``bash`
npm install react-querybuilder-lite
`bash`
yarn add react-querybuilder-lite
`bash`
pnpm add react-querybuilder-lite
Peer Dependencies: React 16.8+
`tsx
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
return ( value={rule.operator} value={rule.value ?? ''}
renderRule={({ rule, fields, operators, onChange, slots }) => (
value={rule.field}
onChange={(e) => onChange({ field: e.target.value })}
>
{fields.map((f) => (
))}
onChange={(e) => onChange({ operator: e.target.value })}
>
{operators.map((op) => (
))}
onChange={(e) => onChange({ value: e.target.value })}
/>
)}
renderGroup={({ group, children, onChange, slots }) => (
value={group.combinator}
onChange={(e) => onChange({ combinator: e.target.value })}
>
{children}
)}
/>
);
}
`
Use QueryBuilder.BuilderWithDnD to enable drag-and-drop reordering.
`tsx
import { QueryBuilder, type Query } from 'react-querybuilder-lite';
{/ ... rest of your UI /} {children}
renderRule={({ rule, fields, operators, onChange, slots }) => (
{/ Drag handle - spread slots.dragHandles on any element /}
⠿
)}
renderGroup={({ group, children, onChange, slots }) => (
⠿
)}
/>
`
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 | 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 |
#### RuleRenderProps
`typescript`
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
}
#### GroupRenderProps
`typescript`
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
}
| 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. |AND
| Combinator | Logical operator to combine rules: or OR. |
`typescript
// The root query structure
type Query = RuleGroup;
interface RuleGroup {
id: string;
combinator: 'and' | 'or';
rules: Array
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 |equal
| Binary | , not_equal, less, less_or_equal, greater, greater_or_equal, contains, starts_with, ends_with |between
| Range | , not_between |in
| List | , 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:
`typescript
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: [
{ 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' },
],
};
operatorsByFieldType={operatorsByFieldType}
renderRule={...}
renderGroup={...}
/>
`
Full internationalization support. You control all user-facing text:
- fields — Translated field labels
- operatorsByFieldType — Translated operator names
- renderRule / renderGroup — Your components, your language. Full control over buttons, placeholders, and combinators
- dragDropAccessibility` — 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