Skip to content

Headless React query builder with compound components, render props, TypeScript & drag-and-drop. Works with any design system.

License

Notifications You must be signed in to change notification settings

NandhakumarE/react-querybuilder-lite

Repository files navigation

react-querybuilder-lite

npm version TypeScript React License Storybook CodeSandbox

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-lite
yarn add react-querybuilder-lite
pnpm add react-querybuilder-lite

Peer 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 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.

Live Demos

📚 View Storybook →

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

About

Headless React query builder with compound components, render props, TypeScript & drag-and-drop. Works with any design system.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •