Skip to main content

useDrag

Location: src/hooks/useDrag.js

Overview

This file exports two hooks for interactive element manipulation:

  • useDrag -- makes an element draggable with mouse and touch support, optional grid snapping, and bounding constraints
  • useResize -- makes an element resizable with mouse support, min/max size constraints, and optional aspect ratio locking

Both hooks provide ref-based binding, meaning you attach the returned ref to the target DOM element.


useDrag

Signature

function useDrag(options?: {
enabled?: boolean,
onDragStart?: () => void,
onDrag?: (x: number, y: number, deltaX: number, deltaY: number) => void,
onDragEnd?: (x: number, y: number) => void,
initialPosition?: { x: number, y: number },
bounds?: { minX?: number, maxX?: number, minY?: number, maxY?: number },
gridSize?: number
}): {
dragRef: React.RefObject<HTMLElement>,
isDragging: boolean,
position: { x: number, y: number },
setPosition: (pos: { x: number, y: number }) => void
}

Parameters

All parameters are optional and passed as a single options object:

ParameterTypeDefaultDescription
enabledbooleantrueWhether dragging is enabled.
onDragStart() => void--Callback fired when the user starts dragging.
onDrag(x, y, deltaX, deltaY) => void--Callback fired during drag with current position and delta from start.
onDragEnd(x, y) => void--Callback fired when the user releases.
initialPosition{ x, y }{ x: 0, y: 0 }Starting position. Updates from external changes when not dragging.
bounds{ minX?, maxX?, minY?, maxY? }--Optional constraints to keep the element within bounds.
gridSizenumber0Snap-to-grid size in pixels. 0 disables snapping.

Return Value

PropertyTypeDescription
dragRefReact.RefObject<HTMLElement>Ref to attach to the draggable element.
isDraggingbooleantrue while the user is actively dragging.
position{ x: number, y: number }Current position of the element.
setPosition(pos) => voidProgrammatically set the position.

Behavior Notes

  • Only left-click triggers drag (ignores right-click and middle-click)
  • Clicks on interactive elements (button, input, textarea, select, a, [data-no-drag]) do not start a drag
  • Text selection is disabled during drag
  • Supports both mouse and single-finger touch events

Usage

function DraggableCard() {
const { dragRef, isDragging, position } = useDrag({
initialPosition: { x: 100, y: 100 },
bounds: { minX: 0, minY: 0, maxX: 800, maxY: 600 },
gridSize: 20
});

return (
<div
ref={dragRef}
style={{
transform: `translate(${position.x}px, ${position.y}px)`,
opacity: isDragging ? 0.8 : 1,
cursor: isDragging ? 'grabbing' : 'grab'
}}
>
Drag me!
</div>
);
}

useResize

Signature

function useResize(options?: {
enabled?: boolean,
onResizeStart?: () => void,
onResize?: (width: number, height: number) => void,
onResizeEnd?: (width: number, height: number) => void,
initialSize?: { width: number, height: number },
minSize?: { width: number, height: number },
maxSize?: { width: number, height: number },
aspectRatio?: number | null
}): {
resizeRef: React.RefObject<HTMLElement>,
isResizing: boolean,
size: { width: number, height: number },
setSize: (size: { width: number, height: number }) => void,
handleResizeStart: (e: MouseEvent) => void
}

Parameters

All parameters are optional and passed as a single options object:

ParameterTypeDefaultDescription
enabledbooleantrueWhether resizing is enabled.
onResizeStart() => void--Callback fired when resize begins.
onResize(width, height) => void--Callback fired during resize with current dimensions.
onResizeEnd(width, height) => void--Callback fired when resize completes.
initialSize{ width, height }{ width: 200, height: 200 }Starting size.
minSize{ width, height }{ width: 100, height: 100 }Minimum allowed dimensions.
maxSize{ width, height }{ width: Infinity, height: Infinity }Maximum allowed dimensions.
aspectRationumber | nullnullIf set (e.g. 16/9), maintains this aspect ratio during resize.

Return Value

PropertyTypeDescription
resizeRefReact.RefObject<HTMLElement>Ref to attach to the resizable element.
isResizingbooleantrue while the user is actively resizing.
size{ width, height }Current dimensions.
setSize(size) => voidProgrammatically set the size.
handleResizeStart(e) => voidAttach this to a resize handle's onMouseDown to initiate resizing.

Usage

function ResizablePanel() {
const { resizeRef, size, isResizing, handleResizeStart } = useResize({
initialSize: { width: 300, height: 200 },
minSize: { width: 150, height: 100 },
aspectRatio: 16 / 9
});

return (
<div
ref={resizeRef}
style={{ width: size.width, height: size.height, position: 'relative' }}
>
<p>Resizable content</p>
<div
className="resize-handle"
onMouseDown={handleResizeStart}
style={{ position: 'absolute', bottom: 0, right: 0, cursor: 'se-resize' }}
/>
</div>
);
}

Last updated: 2026-02-07