useDrag
The useDrag hook enables drag gesture recognition on any DOM element. It handles pointer events, calculates movement, offset, and velocity, providing a clean callback-based API for building draggable interfaces.
Why Use useDrag?
useDrag simplifies drag interactions by:
- Handling all pointer events - Mouse, touch, and pen support
- Calculating movement - Automatic delta and offset tracking
- Providing velocity - Perfect for momentum-based animations
- Cross-browser support - Works consistently across browsers
- Multiple elements - Support for dragging multiple items
Basic Syntax
useDrag(
refs: RefObject<HTMLElement> | RefObject<HTMLElement>[],
callback: (event: DragEvent) => void,
config?: DragConfig
): void
Parameters
- refs (required): Single ref or array of refs to make draggable
- callback (required): Function called during drag with event data
- config (optional): Configuration object for customizing drag behavior
Simple Example
The simplest drag interaction:
import { useRef } from 'react';
import { useDrag, useValue, withSpring, animate } from 'react-ui-animate';
function DraggableBox() {
const ref = useRef(null);
const [x, setX] = useValue(0);
const [y, setY] = useValue(0);
useDrag(ref, ({ down, movement }) => {
if (down) {
setX(movement.x);
setY(movement.y);
} else {
// Spring back to center when released
setX(withSpring(0));
setY(withSpring(0));
}
});
return (
<animate.div
ref={ref}
style={{
translateX: x,
translateY: y,
width: 100,
height: 100,
background: 'teal',
cursor: 'grab',
}}
/>
);
}
Drag Event Data
The callback receives a DragEvent object with rich information:
type DragEvent = {
index: number; // Index of the element being dragged (for arrays)
down: boolean; // True when actively dragging
movement: { x: number; y: number }; // Delta from drag start
offset: { x: number; y: number }; // Absolute offset from element's position
velocity: { x: number; y: number }; // Velocity in pixels per millisecond
event: PointerEvent; // Native pointer event
cancel: () => void; // Cancel pointer capture
};
Real-World Examples
Example 1: Draggable Card with Snap Back
Card that can be dragged and springs back when released:
function DraggableCard() {
const ref = useRef(null);
const [x, setX] = useValue(0);
const [y, setY] = useValue(0);
useDrag(ref, ({ down, movement }) => {
if (down) {
setX(movement.x);
setY(movement.y);
} else {
// Spring back to original position
setX(withSpring(0));
setY(withSpring(0));
}
});
return (
<animate.div
ref={ref}
style={{
translateX: x,
translateY: y,
width: 200,
height: 200,
background: 'white',
borderRadius: 8,
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
cursor: 'grab',
}}
>
<h3>Drag me</h3>
<p>Release to snap back</p>
</animate.div>
);
}
Example 2: Horizontal Slider
Drag to scroll horizontally:
function HorizontalSlider({ children }) {
const ref = useRef(null);
const [x, setX] = useValue(0);
const [startX, setStartX] = useState(0);
useDrag(ref, ({ down, movement }) => {
if (down) {
setX(startX + movement.x);
} else {
setStartX(x);
}
});
return (
<div style={{ overflow: 'hidden', width: '100%' }}>
<animate.div
ref={ref}
style={{
translateX: x,
display: 'flex',
gap: 20,
cursor: 'grab',
}}
>
{children}
</animate.div>
</div>
);
}
Example 3: Drag to Dismiss
Swipe a card away to dismiss it:
function DismissibleCard({ onDismiss }) {
const ref = useRef(null);
const [x, setX] = useValue(0);
const [opacity, setOpacity] = useValue(1);
useDrag(ref, ({ down, movement, velocity }) => {
if (down) {
setX(movement.x);
// Fade out as it moves
setOpacity(1 - Math.abs(movement.x) / 300);
} else {
// If dragged far enough or fast enough, dismiss
if (Math.abs(movement.x) > 200 || Math.abs(velocity.x) > 0.5) {
onDismiss();
} else {
// Otherwise, snap back
setX(withSpring(0));
setOpacity(withSpring(1));
}
}
});
return (
<animate.div
ref={ref}
style={{
translateX: x,
opacity,
width: 300,
height: 200,
background: 'white',
borderRadius: 8,
cursor: 'grab',
}}
>
Swipe to dismiss
</animate.div>
);
}
Example 4: Multiple Draggable Items
Drag multiple items independently:
function DraggableList({ items }) {
const refs = useRef(items.map(() => createRef()));
const [positions, setPositions] = useValue(items.map(() => 0));
useDrag(refs.current, ({ down, movement, index }) => {
if (down) {
const newPositions = [...positions];
newPositions[index] = movement.x;
setPositions(newPositions);
} else {
// Spring back to original position
setPositions(withSpring(items.map(() => 0)));
}
});
return (
<div>
{items.map((item, i) => (
<animate.div
key={item.id}
ref={refs.current[i]}
style={{
translateX: positions[i],
padding: 20,
marginBottom: 10,
background: 'white',
borderRadius: 8,
cursor: 'grab',
}}
>
{item.name}
</animate.div>
))}
</div>
);
}
Configuration Options
Customize drag behavior with the config parameter:
type DragConfig = {
threshold?: number; // Minimum distance (px) before drag starts
axis?: 'x' | 'y'; // Lock movement to single axis
initial?: () => { x: number; y: number }; // Custom initial offset
};
Example: Horizontal-Only Drag
useDrag(
ref,
({ down, movement }) => {
// Only x movement is tracked
if (down) {
setX(movement.x);
} else {
setX(withSpring(0));
}
},
{
axis: 'x', // Lock to horizontal movement
}
);
Example: Drag with Threshold
useDrag(
ref,
({ down, movement }) => {
// Drag only starts after 10px movement
if (down && (Math.abs(movement.x) > 10 || Math.abs(movement.y) > 10)) {
setX(movement.x);
setY(movement.y);
}
},
{
threshold: 10, // 10px threshold
}
);
Using Velocity for Momentum
Velocity is perfect for creating momentum-based animations:
function MomentumDrag() {
const ref = useRef(null);
const [x, setX] = useValue(0);
const [velocity, setVelocity] = useValue(0);
useDrag(ref, ({ down, movement, velocity: dragVelocity }) => {
if (down) {
setX(movement.x);
setVelocity(dragVelocity.x);
} else {
// Continue with momentum
if (Math.abs(velocity) > 0.1) {
setX(withDecay(velocity * 0.1));
} else {
setX(withSpring(0));
}
}
});
return (
<animate.div
ref={ref}
style={{
translateX: x,
width: 100,
height: 100,
background: 'teal',
cursor: 'grab',
}}
/>
);
}
Common Patterns
Pattern 1: Drag with Boundaries
useDrag(ref, ({ down, movement }) => {
if (down) {
// Constrain to boundaries
const constrainedX = Math.max(-100, Math.min(100, movement.x));
const constrainedY = Math.max(-100, Math.min(100, movement.y));
setX(constrainedX);
setY(constrainedY);
} else {
setX(withSpring(0));
setY(withSpring(0));
}
});
Pattern 2: Drag with Rotation
useDrag(ref, ({ down, movement }) => {
if (down) {
setX(movement.x);
setY(movement.y);
// Rotate based on horizontal movement
setRotation(movement.x * 0.1);
} else {
setX(withSpring(0));
setY(withSpring(0));
setRotation(withSpring(0));
}
});
Pattern 3: Drag to Scale
useDrag(ref, ({ down, movement }) => {
if (down) {
setX(movement.x);
setY(movement.y);
// Scale up while dragging
setScale(1.1);
} else {
setX(withSpring(0));
setY(withSpring(0));
setScale(withSpring(1));
}
});
Best Practices
✅ Do
- Always handle both
downand release states - Use
withSpringfor natural release animations - Provide visual feedback (cursor: 'grab' when not dragging, 'grabbing' when dragging)
- Use transforms (
translateX,translateY) instead of layout properties - Consider using velocity for momentum-based interactions
❌ Don't
- Don't forget to handle the release state (users expect feedback)
- Don't use layout properties (
left,top) - causes reflows - Don't make drag too sensitive - use thresholds when needed
- Don't forget accessibility - provide keyboard alternatives when possible
Performance Tips
- Use transforms -
translateX,translateYare GPU-accelerated - Batch updates - Update multiple values together when possible
- Use refs correctly - Pass stable refs, not inline refs
- Debounce expensive operations - If doing heavy calculations in callbacks
Troubleshooting
Drag doesn't work:
- Make sure the ref is attached to the element
- Check that the element is visible and not covered by another element
- Verify the callback is being called (add console.log)
Drag feels laggy:
- Use transforms instead of layout properties
- Check for other heavy operations in your component
- Ensure you're not causing unnecessary re-renders
Multiple drags don't work:
- Make sure you're passing an array of refs
- Verify each ref is unique and stable
- Check the
indexin the callback to identify which element