skills/implementing-drag-drop/references/library-guide.md

9.8 KiB

Drag-and-Drop Library Guide

Table of Contents

Library Comparison

Current Landscape (2025)

Library Status Bundle Size Accessibility TypeScript Best For
@dnd-kit Active ~10KB core Native Modern React apps
react-beautiful-dnd ⚠️ Archived ~30KB Yes Legacy projects
react-dnd Active ~20KB Yes Complex interactions
Pragmatic drag-drop Active ~5KB Native Atlassian products
SortableJS Active ~27KB No Vanilla JS

Note: react-beautiful-dnd was archived in August 2025. Migrate to dnd-kit or Pragmatic drag-drop.

dnd-kit Setup

Installation

# Core packages
npm install @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities

# Optional packages
npm install @dnd-kit/modifiers   # Advanced drag modifiers
npm install @dnd-kit/accessibility  # Enhanced accessibility

Basic Setup

// 1. Import required components
import {
  DndContext,
  closestCenter,
  KeyboardSensor,
  PointerSensor,
  useSensor,
  useSensors
} from '@dnd-kit/core';
import {
  arrayMove,
  SortableContext,
  sortableKeyboardCoordinates,
  verticalListSortingStrategy
} from '@dnd-kit/sortable';

// 2. Configure sensors
const sensors = useSensors(
  useSensor(PointerSensor, {
    activationConstraint: {
      distance: 8, // Prevent accidental activation
    },
  }),
  useSensor(KeyboardSensor, {
    coordinateGetter: sortableKeyboardCoordinates,
  })
);

// 3. Set up context
function App() {
  return (
    <DndContext
      sensors={sensors}
      collisionDetection={closestCenter}
      onDragEnd={handleDragEnd}
    >
      {/* Your draggable content */}
    </DndContext>
  );
}

TypeScript Configuration

// types/dnd.ts
import type { Active, Over } from '@dnd-kit/core';

export interface DragEndEvent {
  active: Active;
  over: Over | null;
}

export interface DraggableItem {
  id: string;
  content: React.ReactNode;
  order?: number;
}

export interface DragState {
  activeId: string | null;
  overId: string | null;
  isDragging: boolean;
}

Core Concepts

Sensors

Sensors detect different input methods for initiating drag operations.

// Pointer Sensor - Mouse and touch
useSensor(PointerSensor, {
  activationConstraint: {
    distance: 8, // Movement threshold
    delay: 250,  // Long press for touch
    tolerance: 5, // Movement tolerance during delay
  },
});

// Keyboard Sensor - Accessibility
useSensor(KeyboardSensor, {
  coordinateGetter: sortableKeyboardCoordinates,
  scrollBehavior: 'smooth',
});

// Custom Touch Sensor
class TouchSensor extends PointerSensor {
  static activators = [
    {
      eventName: 'onTouchStart',
      handler: ({ nativeEvent: event }) => {
        return event.touches.length === 1;
      },
    },
  ];
}

Collision Detection

Algorithms to determine drop targets.

import {
  closestCenter,     // Closest to center point
  closestCorners,    // Closest to any corner
  rectIntersection,  // Rectangle intersection
  pointerWithin,     // Pointer within bounds
} from '@dnd-kit/core';

// Custom collision detection
function customCollisionDetection(args) {
  // First try pointer within
  const pointerCollisions = pointerWithin(args);
  if (pointerCollisions.length > 0) {
    return pointerCollisions;
  }
  // Fallback to closest center
  return closestCenter(args);
}

Modifiers

Transform drag behavior.

import { restrictToVerticalAxis, restrictToWindowEdges } from '@dnd-kit/modifiers';

<DndContext modifiers={[restrictToVerticalAxis, restrictToWindowEdges]}>
  {/* Constrained dragging */}
</DndContext>

Drag Overlay

Custom drag preview.

import { DragOverlay } from '@dnd-kit/core';

function App() {
  const [activeId, setActiveId] = useState(null);

  return (
    <DndContext onDragStart={({active}) => setActiveId(active.id)}>
      {/* Regular content */}
      <DragOverlay>
        {activeId ? <CustomDragPreview id={activeId} /> : null}
      </DragOverlay>
    </DndContext>
  );
}

Migration Guide

From react-beautiful-dnd to dnd-kit

Key Differences

Feature react-beautiful-dnd dnd-kit
API Style Higher-level Lower-level, composable
Drag Preview Built-in Custom via DragOverlay
Animation Automatic Manual control
Accessibility Good Excellent
Touch Support Basic Advanced

Migration Steps

1. Replace Imports

// Before (react-beautiful-dnd)
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';

// After (dnd-kit)
import { DndContext, SortableContext, useSortable } from '@dnd-kit/sortable';

2. Update Context Structure

// Before
<DragDropContext onDragEnd={onDragEnd}>
  <Droppable droppableId="list">
    {(provided) => (
      <div {...provided.droppableProps} ref={provided.innerRef}>
        {items.map((item, index) => (
          <Draggable key={item.id} draggableId={item.id} index={index}>
            {(provided, snapshot) => (
              <div
                ref={provided.innerRef}
                {...provided.draggableProps}
                {...provided.dragHandleProps}
              >
                {item.content}
              </div>
            )}
          </Draggable>
        ))}
        {provided.placeholder}
      </div>
    )}
  </Droppable>
</DragDropContext>

// After
<DndContext sensors={sensors} onDragEnd={handleDragEnd}>
  <SortableContext items={items}>
    {items.map(item => (
      <SortableItem key={item.id} id={item.id}>
        {item.content}
      </SortableItem>
    ))}
  </SortableContext>
</DndContext>

3. Create Sortable Item Component

function SortableItem({ id, children }) {
  const {
    attributes,
    listeners,
    setNodeRef,
    transform,
    transition,
    isDragging,
  } = useSortable({ id });

  const style = {
    transform: CSS.Transform.toString(transform),
    transition,
    opacity: isDragging ? 0.5 : 1,
  };

  return (
    <div ref={setNodeRef} style={style} {...attributes} {...listeners}>
      {children}
    </div>
  );
}

4. Update Event Handlers

// Before
function onDragEnd(result) {
  if (!result.destination) return;

  const items = Array.from(this.state.items);
  const [reorderedItem] = items.splice(result.source.index, 1);
  items.splice(result.destination.index, 0, reorderedItem);

  this.setState({ items });
}

// After
function handleDragEnd(event) {
  const { active, over } = event;

  if (active.id !== over.id) {
    setItems((items) => {
      const oldIndex = items.findIndex(item => item.id === active.id);
      const newIndex = items.findIndex(item => item.id === over.id);
      return arrayMove(items, oldIndex, newIndex);
    });
  }
}

Alternative Libraries

react-dnd

When to Use:

  • Complex drag sources and drop targets
  • Need HTML5 backend flexibility
  • Custom drag layers

Setup:

import { useDrag, useDrop, DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';

function App() {
  return (
    <DndProvider backend={HTML5Backend}>
      {/* Your app */}
    </DndProvider>
  );
}

Pragmatic drag-drop (Atlassian)

When to Use:

  • Minimal bundle size priority
  • Atlassian ecosystem
  • Simple drag operations

Setup:

import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';

// Make element draggable
useEffect(() => {
  const element = ref.current;
  if (!element) return;

  return draggable({
    element,
    getInitialData: () => ({ id: item.id }),
  });
}, [item.id]);

SortableJS

When to Use:

  • Vanilla JavaScript projects
  • Framework agnostic
  • Legacy browser support

Note: Limited accessibility support and no TypeScript.

Best Practices

Performance Optimization

// 1. Memoize expensive calculations
const sortedItems = useMemo(
  () => items.sort((a, b) => a.order - b.order),
  [items]
);

// 2. Use CSS transforms
const style = {
  transform: `translate3d(${x}px, ${y}px, 0)`,
  willChange: 'transform',
};

// 3. Throttle drag events
const throttledDragMove = useThrottle(handleDragMove, 16); // 60fps

Accessibility First

// Always provide keyboard support
const sensors = useSensors(
  useSensor(PointerSensor),
  useSensor(KeyboardSensor, {
    coordinateGetter: sortableKeyboardCoordinates,
  })
);

// Add ARIA attributes
<div
  role="button"
  tabIndex={0}
  aria-roledescription="sortable"
  aria-describedby="DndContext-announcements"
/>

Touch Device Support

// Configure touch activation
useSensor(PointerSensor, {
  activationConstraint: {
    delay: 250, // Long press
    tolerance: 5, // Movement tolerance
  },
});

// Prevent scrolling during drag
useEffect(() => {
  if (isDragging) {
    document.body.style.overflow = 'hidden';
    document.body.style.touchAction = 'none';
  }
  return () => {
    document.body.style.overflow = '';
    document.body.style.touchAction = '';
  };
}, [isDragging]);

Troubleshooting

Common Issues

Issue: Drag not initiating on touch devices

  • Solution: Add delay to activation constraint
  • Check for conflicting touch handlers

Issue: Performance lag with many items

  • Solution: Implement virtualization
  • Use CSS transforms instead of position
  • Memoize components

Issue: Accessibility not working

  • Solution: Ensure keyboard sensor configured
  • Add proper ARIA attributes
  • Test with screen readers

Issue: Scroll not working during drag

  • Solution: Configure auto-scroll
  • Check overflow styles
  • Implement edge detection