Skip to main content

Custom Collision Detection

Collision detection algorithms determine which droppable area a draggable item should interact with during a drag operation. dnd-kit provides several built-in algorithms and allows you to create custom ones for specialized use cases.

Understanding Collision Detection

A collision detector is a function that receives information about the current drag operation and a droppable target, and returns a collision result with a value indicating the strength of the collision.
type CollisionDetector = (input: {
  dragOperation: DragOperation;
  droppable: Droppable;
}) => CollisionResult | null;

type CollisionResult = {
  id: string;
  value: number; // Higher values = stronger collision
  type: CollisionType;
  priority: CollisionPriority;
};

Built-in Algorithms

dnd-kit includes several collision detection algorithms:

closestCenter

Detects the droppable whose center is closest to the drag source. This is ideal for general-purpose drag and drop.
packages/collision/src/algorithms/closestCenter.ts
import {closestCenter} from '@dnd-kit/collision';

const manager = new DragDropManager({
  collisionDetector: closestCenter,
});
Implementation detail: It calculates the distance using Point.distance(droppable.shape.center, shape?.current.center ?? position.current) and returns 1 / distance as the collision value.

pointerIntersection

High-precision algorithm that only detects collisions when the pointer is directly over a droppable element.
packages/collision/src/algorithms/pointerIntersection.ts
import {pointerIntersection} from '@dnd-kit/collision';

const manager = new DragDropManager({
  collisionDetector: pointerIntersection,
});
This algorithm checks if droppable.shape.containsPoint(pointerCoordinates) and has high priority (CollisionPriority.High).

directionBiased

Detects collisions based on movement direction, perfect for sortable lists where you only want to detect items in the direction you’re moving.
packages/collision/src/algorithms/directionBiased.ts
import {directionBiased} from '@dnd-kit/collision';

const manager = new DragDropManager({
  collisionDetector: directionBiased,
});

Custom Collision Detection Examples

Example 1: Zone-Based Collision Detection

Create a collision detector that prioritizes specific zones on your canvas:
import {CollisionDetector, CollisionPriority, CollisionType} from '@dnd-kit/abstract';
import {Point} from '@dnd-kit/geometry';

const zonePriorities = {
  'zone-1': 3,
  'zone-2': 2,
  'zone-3': 1,
};

const zoneBased: CollisionDetector = ({dragOperation, droppable}) => {
  if (!droppable.shape) return null;

  const {shape, position} = dragOperation;
  const dragCenter = shape?.current.center ?? position.current;

  // Check if the drag position is within the droppable bounds
  if (!droppable.shape.containsPoint(dragCenter)) {
    return null;
  }

  // Get zone priority from droppable data
  const zonePriority = droppable.data.get('zone') 
    ? zonePriorities[droppable.data.get('zone')] ?? 1
    : 1;

  // Calculate distance for tie-breaking
  const distance = Point.distance(droppable.shape.center, dragCenter);
  const distanceValue = distance === 0 ? 1 : 1 / distance;

  // Combine zone priority with distance
  const value = zonePriority * 1000 + distanceValue;

  return {
    id: droppable.id,
    value,
    type: CollisionType.Collision,
    priority: CollisionPriority.High,
  };
};

// Usage
const manager = new DragDropManager({
  collisionDetector: zoneBased,
});

const droppable1 = manager.registry.droppables.register(element1, {
  data: new Map([['zone', 'zone-1']]),
});

Example 2: Threshold-Based Collision

Only detect collisions when the draggable overlaps a droppable by a certain percentage:
import {CollisionDetector, CollisionPriority, CollisionType} from '@dnd-kit/abstract';
import {Rectangle} from '@dnd-kit/geometry';

function createThresholdCollisionDetector(threshold: number = 0.5): CollisionDetector {
  return ({dragOperation, droppable}) => {
    const {shape} = dragOperation;
    
    if (!shape || !droppable.shape) return null;

    const dragRect = shape.current.boundingRectangle;
    const dropRect = droppable.shape.boundingRectangle;

    // Calculate intersection area
    const intersectionLeft = Math.max(dragRect.left, dropRect.left);
    const intersectionTop = Math.max(dragRect.top, dropRect.top);
    const intersectionRight = Math.min(dragRect.right, dropRect.right);
    const intersectionBottom = Math.min(dragRect.bottom, dropRect.bottom);

    if (intersectionRight <= intersectionLeft || intersectionBottom <= intersectionTop) {
      return null; // No intersection
    }

    const intersectionArea = 
      (intersectionRight - intersectionLeft) * (intersectionBottom - intersectionTop);
    const dragArea = dragRect.width * dragRect.height;
    const overlapRatio = intersectionArea / dragArea;

    // Only return collision if overlap exceeds threshold
    if (overlapRatio < threshold) {
      return null;
    }

    return {
      id: droppable.id,
      value: overlapRatio,
      type: CollisionType.ShapeIntersection,
      priority: CollisionPriority.Normal,
    };
  };
}

// Usage: Require 75% overlap before detecting collision
const manager = new DragDropManager({
  collisionDetector: createThresholdCollisionDetector(0.75),
});

Example 3: Combining Multiple Algorithms

Chain multiple collision detection strategies with fallback logic:
import {
  CollisionDetector,
  CollisionPriority,
  CollisionType,
} from '@dnd-kit/abstract';
import {pointerIntersection, closestCenter} from '@dnd-kit/collision';

function combineCollisionDetectors(
  ...detectors: CollisionDetector[]
): CollisionDetector {
  return (input) => {
    for (const detector of detectors) {
      const result = detector(input);
      if (result) return result;
    }
    return null;
  };
}

// Try pointer intersection first, fall back to closest center
const hybridDetector = combineCollisionDetectors(
  pointerIntersection,
  closestCenter
);

const manager = new DragDropManager({
  collisionDetector: hybridDetector,
});

Example 4: Grid-Snapping Collision Detection

Detect the nearest grid cell for precise placement:
import {CollisionDetector, CollisionPriority, CollisionType} from '@dnd-kit/abstract';
import {Point} from '@dnd-kit/geometry';

function createGridCollisionDetector(
  gridSize: {x: number; y: number}
): CollisionDetector {
  return ({dragOperation, droppable}) => {
    if (!droppable.shape) return null;

    const {position, shape} = dragOperation;
    const dragPoint = shape?.current.center ?? position.current;

    // Get the grid cell coordinates from droppable data
    const gridX = droppable.data.get('gridX');
    const gridY = droppable.data.get('gridY');

    if (typeof gridX !== 'number' || typeof gridY !== 'number') {
      return null;
    }

    // Calculate the center point of this grid cell
    const cellCenter = {
      x: gridX * gridSize.x + gridSize.x / 2,
      y: gridY * gridSize.y + gridSize.y / 2,
    };

    // Calculate distance from drag point to cell center
    const distance = Point.distance(cellCenter, dragPoint);

    // Only consider cells within a reasonable range
    const maxDistance = Math.sqrt(gridSize.x ** 2 + gridSize.y ** 2);
    if (distance > maxDistance) {
      return null;
    }

    const value = 1 / (distance + 1);

    return {
      id: droppable.id,
      value,
      type: CollisionType.Collision,
      priority: CollisionPriority.Normal,
    };
  };
}

// Usage
const manager = new DragDropManager({
  collisionDetector: createGridCollisionDetector({x: 100, y: 100}),
});

// Register grid cells as droppables
for (let row = 0; row < 10; row++) {
  for (let col = 0; col < 10; col++) {
    const cell = document.querySelector(`[data-grid="${row}-${col}"]`);
    manager.registry.droppables.register(cell, {
      data: new Map([
        ['gridX', col],
        ['gridY', row],
      ]),
    });
  }
}

Best Practices

  1. Return null for non-collisions: Always return null when there’s no collision instead of a result with a value of 0.
  2. Use appropriate priorities: Set CollisionPriority.High for precise collisions (like pointer intersection) and CollisionPriority.Normal for proximity-based detection.
  3. Normalize collision values: Keep collision values in a reasonable range. The built-in algorithms use 1 / distance which provides good resolution.
  4. Guard against null shapes: Always check if droppable.shape exists before accessing its properties.
  5. Consider performance: Collision detection runs frequently during dragging. Keep calculations lightweight.

Collision Types

The library defines several collision types that affect how collisions are processed:
  • CollisionType.Collision - Standard collision detection
  • CollisionType.PointerIntersection - High-precision pointer-based collision
  • CollisionType.ShapeIntersection - Shape-based overlap detection
Higher priority collisions take precedence when multiple droppables are detected.

Build docs developers (and LLMs) love