9.8 KiB
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