DOM

useDraggable

Drag an element with pointer or keyboard: bounds, grid, axis lock, smoothing, and jump-to-click.

Demo

position.x
50.0%
position.y
50.0%
isDragging
false

Press anywhere to jump, hold to keep dragging. Focus the puck and use the arrow keys.

Usage

use-draggable.tsxtsx
import { useDraggable } from "hookli";

export function Demo() {
  const { ref, containerRef, position, isDragging } = useDraggable<
    HTMLDivElement,
    HTMLDivElement
  >({
    unit: "ratio",
    bounds: "parent",
    initialPosition: { x: 0.5, y: 0.5 },
    jumpOnClick: true,
  });

  return (
    <div ref={containerRef} style={{ position: "relative", height: 224 }}>
      <div
        ref={ref}
        tabIndex={0}
        aria-label="Draggable puck"
        style={{ position: "absolute", top: 0, left: 0, width: 44, height: 44 }}
      >
        {isDragging ? "●" : "○"}
      </div>
      <p>
        {(position.x * 100).toFixed(1)}% × {(position.y * 100).toFixed(1)}%
      </p>
    </div>
  );
}

API

useDraggable.d.tsts
useDraggable<T extends HTMLElement, C extends HTMLElement>(options?: UseDraggableOptions): UseDraggableReturn<T, C>

Parameters

NameTypeDefaultDescription
optionsUseDraggableOptions{}Every option is optional. See the UseDraggableOptions table below.

Returns

NameTypeDescription
ref(node: T | null) => voidCallback ref for the draggable element. The hook moves it with transform: translate3d(), so layout is untouched.
containerRef(node: C | null) => voidCallback ref for the canvas that jumpOnClick listens on.
positionPositionThe current position in unit. In controlled mode it is the position prop.
isDraggingbooleantrue while a pointer drag is in progress.
moveTo(position: Position, options?: { animate?: boolean }) => voidMove programmatically. Bounds and grid still apply; in controlled mode it calls onChange instead.
reset() => voidMove back to initialPosition.

UseDraggableOptions

Pass position + onChange for controlled use, or initialPosition for uncontrolled.

NameTypeDescription
unit"px" | "ratio"px: translate offset from the layout spot. ratio: where the anchor point sits in the bounds box, 0..1 per axis, so it survives resizes. Needs element bounds.
positionPositionControlled position. Input calls onChange; the element moves only when this prop changes.
initialPositionPositionUncontrolled starting position.
onChange(position: Position) => voidCalled when a drag, jump, arrow key or moveTo produces a new position. Not called for prop changes or resize re-clamps.
bounds"parent" | RefObject<HTMLElement> | { left?, top?, right?, bottom? }Keep the element inside a box. Element bounds are live: re-measured on every gesture and on resize.
axis"x" | "y" | "both"Restrict movement to one axis.
grid[number, number]Snap the translate offset to a px grid.
jumpOnClickbooleanPressing an empty spot of the container jumps the element there; holding keeps dragging.
anchor"center" | "top-left"The element point that lands on the pointer when jumping, and that a ratio position describes.
transitionnumberJump animation length in ms; 0 snaps instantly.
smoothingnumberHow softly the element trails the pointer, from 0 (glued) to just under 1 (floaty). Reduced motion turns it off.
keyboardStepnumber | falseArrow-key step in px when the element is focused; the grid step wins when grid is set.
disabledbooleanIgnore pointer and keyboard input. moveTo still works.
onDragStart / onDrag / onDragEnd / onJump(position: Position) => voidGesture callbacks, in unit. onDragEnd also fires after each keyboard move.

Position

A point in the chosen unit.

NameTypeDescription
xnumberHorizontal: px offset, or 0..1 of the bounds box width.
ynumberVertical: px offset, or 0..1 of the bounds box height.

View source on GitHub

Related hooks