Skip to main content
Version: Next

Drag-and-drop

The @react-querybuilder/dnd package augments React Query Builder with drag-and-drop functionality. Click here for demo.

Usage

To enable drag-and-drop on a query builder, render the QueryBuilderDnD context provider higher in the component tree than QueryBuilder.

The @react-querybuilder/dnd package supports multiple drag-and-drop libraries through an adapter pattern. Each adapter is available as a separate subpath import (e.g., @react-querybuilder/dnd/react-dnd), so only the adapter you use—and its corresponding library—needs to be installed.

Note: The enableDragAndDrop prop doesn't need to be set directly on the QueryBuilder component unless it's explicitly false to override the implicit true value set by QueryBuilderDnD.

When the enableDragAndDrop prop is true, a drag handle appears on the left side of each rule and group header. Clicking and dragging the handle element allows users to visually reorder rules and groups.

Using the @atlaskit/pragmatic-drag-and-drop adapter

Install @atlaskit/pragmatic-drag-and-drop, then create an adapter with createPragmaticDndAdapter:

npm i react-querybuilder @react-querybuilder/dnd @atlaskit/pragmatic-drag-and-drop
import { QueryBuilderDnD } from '@react-querybuilder/dnd';
import { createPragmaticDndAdapter } from '@react-querybuilder/dnd/pragmatic-dnd';
import {
draggable,
dropTargetForElements,
monitorForElements,
} from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
import { QueryBuilder } from 'react-querybuilder';

const pragmaticDndAdapter = createPragmaticDndAdapter({
draggable,
dropTargetForElements,
monitorForElements,
combine,
});

const App = () => (
<QueryBuilderDnD dnd={pragmaticDndAdapter}>
<QueryBuilder />
</QueryBuilderDnD>
);

Pragmatic drag and drop uses the native HTML5 drag-and-drop API under the hood, so it has zero runtime overhead when not dragging. Unlike react-dnd, it does not require a separate backend package.

Using the @dnd-kit adapter

Install @dnd-kit/core, then create an adapter with createDndKitAdapter:

npm i react-querybuilder @react-querybuilder/dnd @dnd-kit/core
import { QueryBuilderDnD } from '@react-querybuilder/dnd';
import { createDndKitAdapter } from '@react-querybuilder/dnd/dnd-kit';
import * as DndKit from '@dnd-kit/core';
import { QueryBuilder } from 'react-querybuilder';

const dndKitAdapter = createDndKitAdapter(DndKit);

const App = () => (
<QueryBuilderDnD dnd={dndKitAdapter}>
<QueryBuilder />
</QueryBuilderDnD>
);

The dnd-kit adapter uses PointerSensor and KeyboardSensor by default, with a 5px activation distance to prevent accidental drags. ARIA attributes are automatically applied to drag handles for accessibility.

Using the react-dnd adapter

Install react-dnd and either react-dnd-html5-backend or react-dnd-touch-backend (or both), then create an adapter with createReactDnDAdapter:

npm i react-querybuilder @react-querybuilder/dnd react-dnd react-dnd-html5-backend react-dnd-touch-backend
import { QueryBuilderDnD } from '@react-querybuilder/dnd';
import { createReactDnDAdapter } from '@react-querybuilder/dnd/react-dnd';
import * as ReactDnD from 'react-dnd';
import * as ReactDndHtml5Backend from 'react-dnd-html5-backend';
import * as ReactDndTouchBackend from 'react-dnd-touch-backend';
import { QueryBuilder } from 'react-querybuilder';

const reactDnDAdapter = createReactDnDAdapter({
...ReactDnD,
...ReactDndHtml5Backend,
...ReactDndTouchBackend,
});

const App = () => (
<QueryBuilderDnD dnd={reactDnDAdapter}>
<QueryBuilder />
</QueryBuilderDnD>
);
Legacy API

For backward compatibility, you can still pass the raw react-dnd exports directly. They will be automatically wrapped in an adapter:

<QueryBuilderDnD dnd={{ ...ReactDnD, ...ReactDndHtml5Backend, ...ReactDndTouchBackend }}>
<QueryBuilder />
</QueryBuilderDnD>

Zero-config (auto-loading)

If you omit the dnd prop, QueryBuilderDnD will asynchronously attempt to load react-dnd, react-dnd-html5-backend, and react-dnd-touch-backend. Drag-and-drop features are only enabled once those packages have loaded. This approach is convenient but provides less control over loading behavior.

Styling

The default stylesheet applies special styles during drag-and-drop operations to help anticipate the result of the impending drop. For more information, see Styling overview § Drag-and-drop.

Cloning and grouping

By default, a successful drag-and-drop action moves a rule or group to a new location within the query builder, but there are three alternative behaviors:

  • Clone: Hold down the Alt key (⌥ Option on macOS) before and during the drag to clone the dragged rule/group instead of moving it. The original object remains in place and the clone is inserted at the drop location.
  • Group: Hold down the Ctrl key before and during the drag to create a new group at the drop location. The new group's rules array contains the rule or group originally at the drop location and the dragged rule/group, in that order. (This operation is similar to how new folders are created on iOS by dragging one app icon on top of another.)
  • Clone into group: Hold down the Alt/⌥ Option and Ctrl keys before and during the drag to create a new group at the drop location and clone the dragged rule/group into the new group instead of moving it.

The keys that determine alternate behaviors are configurable. See copyModeModifierKey and groupModeModifierKey.

As a keyboard-free alternative, you can configure timer-based activation with copyModeAfterHoverMs and groupModeAfterHoverMs. When set, hovering over a drop target for the specified duration automatically activates copy or group mode. Both methods are additive — modifier keys still work alongside timers.

Existing drag-and-drop contexts

If your application already uses react-dnd, use QueryBuilderDndWithoutProvider instead of QueryBuilderDnD. They are functionally equivalent, but the former assumes a <DndProvider /> already exists higher in the component tree. The latter renders its own DndProvider which conflicts with any pre-existing ones. (If you use the wrong component, you'll probably see the error message "Cannot have two HTML5 backends at the same time" in the console.)

Custom adapters

You can create a custom adapter for any drag-and-drop library by implementing the DndAdapter interface:

import type { DndAdapter } from '@react-querybuilder/dnd';

const myAdapter: DndAdapter = {
// Context provider wrapping the query builder tree
DndProvider: ({ debugMode, children }) => <MyDndContext>{children}</MyDndContext>,

// Hook providing drag-and-drop behavior for rule components
useRuleDnD: params => {
// Implement using your DnD library's primitives
// Must return: isDragging, dragMonitorId, isOver, dropMonitorId, dragRef, dndRef, dropEffect?, groupItems?, dropNotAllowed?
},

// Hook providing drag-and-drop behavior for rule group components
useRuleGroupDnD: params => {
// Must return: isDragging, dragMonitorId, isOver, dropMonitorId, previewRef, dragRef, dropRef, dropEffect?, groupItems?, dropNotAllowed?
},

// Hook providing drop-target behavior for inline combinators
useInlineCombinatorDnD: params => {
// Must return: isOver, dropMonitorId, dropRef, dropEffect?, groupItems?, dropNotAllowed?
},
};

The shared logic functions canDropOnRule, canDropOnRuleGroup, canDropOnInlineCombinator, buildDropResult, and handleDrop are exported from @react-querybuilder/dnd and can be used in custom adapter implementations to ensure consistent drop validation and behavior.

Props

The following props are accepted on the QueryBuilderDnD and QueryBuilderDndWithoutProvider components.

dnd

DndAdapter | DndProp

A DndAdapter (such as from createReactDnDAdapter(), createDndKitAdapter(), or createPragmaticDndAdapter()) or the raw react-dnd namespace exports (legacy API). When raw react-dnd exports are provided, they are automatically wrapped in a react-dnd adapter.

If omitted, the component asynchronously loads react-dnd, react-dnd-html5-backend, and react-dnd-touch-backend. Drag-and-drop features are only enabled once those packages have loaded.

When both backends are provided, the touch backend is preferred when a touch device is detected.

canDrop

(params: { dragging: RuleGroupTypeAny, hovering: RuleGroupTypeAny, groupItems?: boolean }) => boolean

This function determines whether a "drop" at the current hover location is valid during a drag operation. The dragging and hovering properties represent the dragged rule/group and the currently hovered rule/group, respectively. Each property includes the object's original path and qbId. groupItems is true if the groupModeModifierKey is currently pressed.

copyModeModifierKey

string

Key code for the modifier key that puts a drag-and-drop action in "copy" mode. Default is "alt" (Alt on Windows and Linux, ⌥ Option on macOS).

copyModeAfterHoverMs

number | undefined

Milliseconds after hovering a drop target before the drop effect automatically switches to "copy" mode. This provides a keyboard-free alternative to holding the copyModeModifierKey — both methods are additive, so either one activates copy mode. The timer resets when the drag moves to a different target. Set to undefined or 0 to disable (default).

Supported by the @atlaskit/pragmatic-drag-and-drop and @dnd-kit/core adapters. The react-dnd adapter ignores this prop.

groupModeModifierKey

string

Key code for the modifier key that puts a drag-and-drop action in "group" mode. Default is "ctrl" (Ctrl).

groupModeAfterHoverMs

number | undefined

Milliseconds after hovering a drop target before the drop will automatically create a new group ("group" mode). This provides a keyboard-free alternative to holding the groupModeModifierKey — both methods are additive, so either one activates group mode. The timer resets when the drag moves to a different target. Set to undefined or 0 to disable (default).

Supported by the @atlaskit/pragmatic-drag-and-drop and @dnd-kit/core adapters. The react-dnd adapter ignores this prop.

hideDefaultDragPreview

boolean

When true, disables the default browser drag preview during drag operations. This is useful when implementing custom drag layers or custom drag preview components (see relevant React DnD documentation). Default is false.

updateWhileDragging

boolean

When true, the query tree visually rearranges in real-time as the user drags rules and groups, providing immediate spatial feedback instead of showing a drop indicator line. The actual onQueryChange callback only fires once when the item is dropped — intermediate positions are purely visual. If the drag is cancelled (e.g., by releasing outside a valid target), the query reverts to its original state.

Currently supported by the @atlaskit/pragmatic-drag-and-drop and @dnd-kit/core adapters. The react-dnd adapter ignores this prop and falls back to the standard drop-indicator behavior.

When this feature is active:

  • Rules and groups slide into their preview positions during drag
  • The standard "drop indicator" line (dndOver class) is suppressed
  • Inline combinator drop targets are disabled (quadrant detection on rules is used instead)
  • Hovering in the upper half of a rule inserts the dragged item before it; the lower half inserts after

Default is false.

<QueryBuilderDnD dnd={pragmaticDndAdapter} updateWhileDragging>
<QueryBuilder />
</QueryBuilderDnD>

onDragMove

(params: { draggedItem, shadowQuery, originalQuery, previewPath }) => void

Callback invoked on each drag position change when updateWhileDragging is true. Receives the current shadow query (the preview query with the dragged item at its prospective position), the original query, and the preview path.