npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-querybuilder-lite

v1.1.0

Published

A lightweight, headless React query builder with drag-and-drop and Typescript support. Build complex filters visually.

Readme

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