Skip to main content

React Drag and Drop Tutorial

Draggable components allow users to move elements freely across the screen, a fundamental interaction pattern in modern web apps. Building drag-and-drop in React means tracking pointer down, move, and up events; calculating position deltas; and updating component position in real time. You can implement this from scratch with pointer events or use libraries like react-beautiful-dnd for complex lists—but understanding the core mechanics ensures you can debug, optimize, and extend the pattern.

Building a Basic Draggable Component

A draggable component needs three pieces: initial offset calculation, pointer tracking, and position updates. Here's a minimal example:

import React, { useState, useRef } from 'react';

export default function DraggableBox() {
const [position, setPosition] = useState({ x: 0, y: 0 });
const [isDragging, setIsDragging] = useState(false);
const dragStartRef = useRef({ x: 0, y: 0 });

const handlePointerDown = (e) => {
setIsDragging(true);
dragStartRef.current = {
x: e.clientX - position.x,
y: e.clientY - position.y,
};
};

const handlePointerMove = (e) => {
if (!isDragging) return;

const newX = e.clientX - dragStartRef.current.x;
const newY = e.clientY - dragStartRef.current.y;
setPosition({ x: newX, y: newY });
};

const handlePointerUp = () => {
setIsDragging(false);
};

return (
<div
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
onPointerLeave={handlePointerUp}
style={{
position: 'absolute',
left: `${position.x}px`,
top: `${position.y}px`,
width: '80px',
height: '80px',
backgroundColor: isDragging ? '#4f46e5' : '#6366f1',
borderRadius: '8px',
cursor: isDragging ? 'grabbing' : 'grab',
transition: isDragging ? 'none' : 'background-color 0.2s',
userSelect: 'none',
}}
>
Drag me
</div>
);
}

The key insight: store the initial offset between the pointer and the element's current position, then use that offset to calculate the new position on every move. Using onPointerLeave ensures dragging stops if the user moves the pointer outside the window.

Constraining Drag to a Container

In many applications, you want to restrict dragging to a parent container—like a modal or a canvas area. Use the container's bounding rect to clamp the position:

import React, { useState, useRef } from 'react';

export default function ConstrainedDraggable() {
const [position, setPosition] = useState({ x: 0, y: 0 });
const [isDragging, setIsDragging] = useState(false);
const dragStartRef = useRef({ x: 0, y: 0 });
const containerRef = useRef(null);

const handlePointerDown = (e) => {
setIsDragging(true);
dragStartRef.current = {
x: e.clientX - position.x,
y: e.clientY - position.y,
};
};

const handlePointerMove = (e) => {
if (!isDragging || !containerRef.current) return;

const container = containerRef.current.getBoundingClientRect();
let newX = e.clientX - dragStartRef.current.x;
let newY = e.clientY - dragStartRef.current.y;

// Clamp to container bounds (element is 80x80)
const boxSize = 80;
newX = Math.max(0, Math.min(newX, container.width - boxSize));
newY = Math.max(0, Math.min(newY, container.height - boxSize));

setPosition({ x: newX, y: newY });
};

const handlePointerUp = () => {
setIsDragging(false);
};

return (
<div
ref={containerRef}
style={{
position: 'relative',
width: '100%',
height: '400px',
border: '2px solid #ccc',
borderRadius: '8px',
overflow: 'hidden',
backgroundColor: '#f9f9f9',
}}
>
<div
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
onPointerLeave={handlePointerUp}
style={{
position: 'absolute',
left: `${position.x}px`,
top: `${position.y}px`,
width: '80px',
height: '80px',
backgroundColor: '#6366f1',
borderRadius: '8px',
cursor: isDragging ? 'grabbing' : 'grab',
}}
>
Constrained
</div>
</div>
);
}

Use getBoundingClientRect() to get the container's dimensions, then clamp newX and newY between 0 and the container's width/height minus the element size. This prevents dragging outside the visible area.

Creating a Draggable List Item Hook

Reusable logic is essential in production apps. Extract drag handling into a custom hook:

import { useState, useRef } from 'react';

export function useDraggable(onDrop) {
const [isDragging, setIsDragging] = useState(false);
const dragStartRef = useRef({ x: 0, y: 0, offsetX: 0, offsetY: 0 });

const handleDown = (e) => {
setIsDragging(true);
const rect = e.currentTarget.getBoundingClientRect();
dragStartRef.current = {
x: e.clientX,
y: e.clientY,
offsetX: e.clientX - rect.left,
offsetY: e.clientY - rect.top,
};
};

const handleUp = (e) => {
if (isDragging && onDrop) {
onDrop({
clientX: e.clientX,
clientY: e.clientY,
});
}
setIsDragging(false);
};

return {
isDragging,
onPointerDown: handleDown,
onPointerUp: handleUp,
dragStart: dragStartRef.current,
};
}

This hook encapsulates drag state and callbacks, making it reusable across multiple components.

Key Takeaways

  • Track the offset between pointer and element on down, then use that offset to compute smooth position on move.
  • Use onPointerLeave in addition to onPointerUp to stop dragging when the pointer leaves the window.
  • Clamp position to container bounds using Math.max and Math.min to prevent dragging outside.
  • Extract drag logic into custom hooks for reusability across components.

Frequently Asked Questions

How do I make dragging smooth without stuttering?

Avoid recalculating container dimensions on every frame. Cache the container rect in a ref and update it only when the container size changes (using ResizeObserver). Additionally, use transform: translate() instead of left/top for better performance, since transform doesn't trigger layout recalculation.

Can I drag multiple items at once?

Track the pointerType and isPrimary. If isPrimary is true, start a drag. For multi-select, store a list of selected item IDs in state, then move all of them together. Alternatively, use React's context API to share drag state across components.

How do I add visual feedback while dragging?

Use the isDragging state to change the cursor (cursor: grabbing), opacity, shadow, or background color. You can also render a "ghost" image at the cursor position showing what's being dragged. The setDragImage() method on the pointer event lets you set a custom drag image (though it's native drag-drop specific, not pointer-event dragging).

Should I use pointer events or the HTML Drag and Drop API?

Pointer events are simpler for custom UI and give you more control. The HTML Drag and Drop API is better for integrating with the browser's file drop and cross-window dragging. Use pointer events for in-app dragging; use Drag and Drop API for file uploads and cross-browser data transfer.

How do I animate a dragged element back to its original position?

After the drop, don't immediately reset position. Instead, use a CSS transition: set transition: transform 0.3s ease-out when not dragging, then use transform: translate() for the return animation. This provides smooth, natural motion.

Further Reading