Skip to main content
Sensors detect and handle user input for drag operations. While dnd-kit provides built-in sensors for pointer, keyboard, and native drag events, you can create custom sensors for specialized interactions.

Understanding Sensors

Sensors extend the base Sensor class and implement input detection:
import {Sensor} from '@dnd-kit/abstract';
import type {CleanupFunction} from '@dnd-kit/state';
import type {DragDropManager, Draggable} from '@dnd-kit/dom';

export class CustomSensor extends Sensor<DragDropManager, CustomSensorOptions> {
  constructor(
    public manager: DragDropManager,
    public options?: CustomSensorOptions
  ) {
    super(manager, options);
  }

  // Bind sensor to a draggable element
  public bind(source: Draggable, options?: CustomSensorOptions): CleanupFunction {
    // Set up event listeners
    const cleanup = () => {
      // Remove event listeners
    };
    
    return cleanup;
  }

  // Cleanup when sensor is destroyed
  public destroy() {
    // Remove any global listeners
  }
}

Built-in Sensors

Pointer Sensor

Handles mouse, touch, and pen input:
import {PointerSensor} from '@dnd-kit/dom/sensors/pointer';

const customPointer = PointerSensor.configure({
  // Activation constraints
  activationConstraints: (event, source) => {
    if (event.pointerType === 'touch') {
      return [
        new PointerActivationConstraints.Delay({value: 250, tolerance: 5}),
      ];
    }
    return undefined;  // No constraints for mouse
  },
  
  // Prevent activation on certain elements
  preventActivation: (event, source) => {
    const target = event.target;
    return target.tagName === 'BUTTON' || target.tagName === 'A';
  },
});

Keyboard Sensor

Enables keyboard-based drag operations:
import {KeyboardSensor} from '@dnd-kit/dom/sensors/keyboard';

const customKeyboard = KeyboardSensor.configure({
  // Movement offset per keypress
  offset: {x: 20, y: 20},
  
  // Custom key mappings
  keyboardCodes: {
    start: ['Space'],
    cancel: ['Escape'],
    end: ['Space', 'Enter'],
    up: ['ArrowUp', 'KeyW'],
    down: ['ArrowDown', 'KeyS'],
    left: ['ArrowLeft', 'KeyA'],
    right: ['ArrowRight', 'KeyD'],
  },
});

Drag Sensor

Uses native HTML drag and drop API:
import {DragSensor} from '@dnd-kit/dom/sensors/drag';

<DragDropProvider sensors={[DragSensor]}>
  {/* Your content */}
</DragDropProvider>

Creating a Custom Sensor

Let’s build a custom sensor that activates drag with a double-click:

Step 1: Define the Sensor Class

import {Sensor, configurator} from '@dnd-kit/abstract';
import {effect} from '@dnd-kit/state';
import type {CleanupFunction} from '@dnd-kit/state';
import {Listeners, getEventCoordinates} from '@dnd-kit/dom/utilities';
import type {DragDropManager, Draggable} from '@dnd-kit/dom';

export interface DoubleClickSensorOptions {
  /**
   * Maximum time between clicks (ms)
   */
  delay?: number;
}

const defaults = {
  delay: 300,
};

export class DoubleClickSensor extends Sensor<
  DragDropManager,
  DoubleClickSensorOptions
> {
  private listeners = new Listeners();
  private lastClickTime = 0;

  constructor(
    public manager: DragDropManager,
    public options?: DoubleClickSensorOptions
  ) {
    super(manager, options);
  }

  public bind(source: Draggable, options = this.options): CleanupFunction {
    const {delay = defaults.delay} = options ?? {};

    const unbind = effect(() => {
      const element = source.handle ?? source.element;
      if (!element) return;

      const handleClick = (event: MouseEvent) => {
        if (source.disabled) return;

        const now = Date.now();
        const timeSinceLastClick = now - this.lastClickTime;

        if (timeSinceLastClick < delay) {
          // Double click detected
          this.handleDoubleClick(event, source);
          this.lastClickTime = 0;
        } else {
          // First click
          this.lastClickTime = now;
        }
      };

      element.addEventListener('click', handleClick);

      return () => {
        element.removeEventListener('click', handleClick);
      };
    });

    return unbind;
  }

  private handleDoubleClick(event: MouseEvent, source: Draggable) {
    event.preventDefault();
    event.stopPropagation();

    const coordinates = getEventCoordinates(event);
    if (!coordinates) return;

    // Start drag operation
    const controller = this.manager.actions.start({
      event,
      coordinates,
      source,
    });

    if (controller.signal.aborted) return;

    // Set up listeners for drag movement and end
    this.setupDragListeners(source);
  }

  private setupDragListeners(source: Draggable) {
    const cleanup = this.listeners.bind(document, [
      {
        type: 'mousemove',
        listener: (event: MouseEvent) => this.handleMove(event),
      },
      {
        type: 'mouseup',
        listener: (event: MouseEvent) => this.handleEnd(event),
      },
      {
        type: 'keydown',
        listener: (event: KeyboardEvent) => {
          if (event.code === 'Escape') {
            this.handleEnd(event, true);
          }
        },
      },
    ]);

    return cleanup;
  }

  private handleMove(event: MouseEvent) {
    const coordinates = getEventCoordinates(event);
    if (!coordinates) return;

    if (this.manager.dragOperation.status.idle) {
      this.manager.actions.start({event, coordinates});
    } else {
      this.manager.actions.move({to: coordinates});
    }
  }

  private handleEnd(event: Event, canceled = false) {
    this.manager.actions.stop({event, canceled});
    this.listeners.clear();
  }

  public destroy() {
    this.listeners.clear();
  }

  static configure = configurator(DoubleClickSensor);
}

Step 2: Use the Custom Sensor

import {DragDropProvider} from '@dnd-kit/react';
import {DoubleClickSensor} from './DoubleClickSensor';

function App() {
  return (
    <DragDropProvider
      sensors={[
        DoubleClickSensor.configure({delay: 400}),
      ]}
    >
      {/* Your draggable content */}
    </DragDropProvider>
  );
}

Activation Constraints

Add constraints to control when dragging activates:

Delay Constraint

import {PointerSensor} from '@dnd-kit/dom/sensors/pointer';
import {PointerActivationConstraints} from '@dnd-kit/dom/sensors/pointer';

const pointerWithDelay = PointerSensor.configure({
  activationConstraints: [
    // Must hold for 250ms within 5px tolerance
    new PointerActivationConstraints.Delay({value: 250, tolerance: 5}),
  ],
});

Distance Constraint

const pointerWithDistance = PointerSensor.configure({
  activationConstraints: [
    // Must move at least 10px to activate
    new PointerActivationConstraints.Distance({value: 10}),
  ],
});

Combined Constraints

const combinedConstraints = PointerSensor.configure({
  activationConstraints: (event, source) => {
    if (event.pointerType === 'touch') {
      // Touch requires delay
      return [
        new PointerActivationConstraints.Delay({value: 250, tolerance: 5}),
      ];
    }
    
    // Mouse requires distance
    return [
      new PointerActivationConstraints.Distance({value: 5}),
    ];
  },
});

Sensor Utilities

The @dnd-kit/dom/utilities package provides helpful utilities:

Event Coordinates

import {getEventCoordinates} from '@dnd-kit/dom/utilities';

const coordinates = getEventCoordinates(event);
// Returns {x: number, y: number} or undefined

Event Listeners Manager

import {Listeners} from '@dnd-kit/dom/utilities';

const listeners = new Listeners();

// Bind multiple listeners
listeners.bind(element, [
  {type: 'mousedown', listener: handleMouseDown},
  {type: 'mousemove', listener: handleMouseMove},
  {type: 'mouseup', listener: handleMouseUp, options: {capture: true}},
]);

// Clean up all listeners
listeners.clear();

Type Guards

import {
  isPointerEvent,
  isKeyboardEvent,
  isElement,
  isHTMLElement,
  isTextInput,
  isInteractiveElement,
} from '@dnd-kit/dom/utilities';

if (isPointerEvent(event)) {
  console.log(event.clientX, event.clientY);
}

Using Multiple Sensors

Combine multiple sensors for comprehensive input support:
import {PointerSensor} from '@dnd-kit/dom/sensors/pointer';
import {KeyboardSensor} from '@dnd-kit/dom/sensors/keyboard';
import {DoubleClickSensor} from './DoubleClickSensor';

<DragDropProvider
  sensors={[
    PointerSensor.configure({/* options */}),
    KeyboardSensor.configure({/* options */}),
    DoubleClickSensor.configure({delay: 300}),
  ]}
>
  {/* Your content */}
</DragDropProvider>
When using multiple sensors, ensure they don’t conflict. For example, both PointerSensor and DoubleClickSensor listening to click events might interfere.

Per-Entity Sensor Configuration

Configure sensors differently for specific draggable items:
function SortableItem({id, index, requiresDoubleClick}) {
  const [element, setElement] = useState(null);
  
  const {isDragging} = useSortable({
    id,
    index,
    element,
    sensors: requiresDoubleClick
      ? [DoubleClickSensor.configure({delay: 400})]
      : undefined,  // Use default sensors
  });
  
  return <div ref={setElement}>{id}</div>;
}

Advanced: Gamepad Sensor Example

Here’s a more complex example using the Gamepad API:
export class GamepadSensor extends Sensor<DragDropManager> {
  private gamepadIndex: number | null = null;
  private animationFrame: number | null = null;

  public bind(source: Draggable): CleanupFunction {
    const handleGamepadConnected = (event: GamepadEvent) => {
      this.gamepadIndex = event.gamepad.index;
      this.startPolling(source);
    };

    const handleGamepadDisconnected = () => {
      this.stopPolling();
      this.gamepadIndex = null;
    };

    window.addEventListener('gamepadconnected', handleGamepadConnected);
    window.addEventListener('gamepaddisconnected', handleGamepadDisconnected);

    return () => {
      window.removeEventListener('gamepadconnected', handleGamepadConnected);
      window.removeEventListener('gamepaddisconnected', handleGamepadDisconnected);
      this.stopPolling();
    };
  }

  private startPolling(source: Draggable) {
    const poll = () => {
      if (this.gamepadIndex === null) return;

      const gamepad = navigator.getGamepads()[this.gamepadIndex];
      if (!gamepad) return;

      // Map joystick to movement
      const [x, y] = gamepad.axes;
      const threshold = 0.2;

      if (Math.abs(x) > threshold || Math.abs(y) > threshold) {
        if (this.manager.dragOperation.status.idle) {
          const element = source.element;
          if (!element) return;
          
          const rect = element.getBoundingClientRect();
          this.manager.actions.start({
            event: null,
            coordinates: {x: rect.left, y: rect.top},
            source,
          });
        } else {
          this.manager.actions.move({
            by: {x: x * 10, y: y * 10},
          });
        }
      }

      // Check for drop button (button 0)
      if (gamepad.buttons[0]?.pressed && !this.manager.dragOperation.status.idle) {
        this.manager.actions.stop({event: null});
      }

      this.animationFrame = requestAnimationFrame(poll);
    };

    this.animationFrame = requestAnimationFrame(poll);
  }

  private stopPolling() {
    if (this.animationFrame !== null) {
      cancelAnimationFrame(this.animationFrame);
      this.animationFrame = null;
    }
  }

  public destroy() {
    this.stopPolling();
  }
}
When building custom sensors, use the effect function from @dnd-kit/state to automatically clean up when elements are removed from the DOM.

Best Practices

1

Always Clean Up

Return cleanup functions from bind() and implement destroy() to prevent memory leaks
2

Respect Disabled State

Check source.disabled before initiating drag operations
3

Handle Edge Cases

Test with missing elements, rapid interactions, and simultaneous input
4

Provide Configuration

Use the configurator helper to allow per-instance customization

Next Steps

1

Explore Source Code

Study the built-in sensors in packages/dom/src/core/sensors/
2

Test Thoroughly

Test custom sensors across different devices and input methods
3

Share Your Sensor

Consider publishing reusable sensors as npm packages

Build docs developers (and LLMs) love