react-querybuilder-lite
v1.1.0
Published
A lightweight, headless React query builder with drag-and-drop and Typescript support. Build complex filters visually.
Maintainers
Readme
react-querybuilder-lite
A lightweight, headless React query builder with drag-and-drop support. Build complex filter UIs with any design system — zero styling opinions.
Why This Library?
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
Features
| 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 |
Installation
npm install react-querybuilder-liteyarn add react-querybuilder-litepnpm add react-querybuilder-litePeer Dependencies: React 16.8+
Quick Start
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>
);
}With Drag & Drop
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>API Reference
<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 |
<QueryBuilder.Builder>
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 |
<QueryBuilder.BuilderWithDnD>
Same props as Builder, plus optional drag preview customization.
| Prop | Type | Required | Description |
|------|------|----------|-------------|
| renderDragPreview | (props: DragPreviewProps) => ReactNode | No | Custom drag overlay |
Render Props
RuleRenderProps
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
}GroupRenderProps
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
}Terminology
| 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. |
Types
Core Types
// 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
}Operators
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 |
Custom Field Types
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>Localization (i18n)
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.
Live Demos
Interactive examples showcasing all components with different configurations.
Design Decisions
| 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 |
Contributing
Contributions are welcome! Please open an issue or submit a PR.
License
MIT
