@neodrag/react

@neodrag/react

A lightweight react hook to make your elements draggable.

npm i @neodrag/react@next

Usage

Basic usage

import { useRef } from 'react';
import { useDraggable } from '@neodrag/react';

function App() {
  const draggableRef = useRef<HTMLDivElement>(null);
  useDraggable(draggableRef);

  return <div ref={draggableRef}>Hello</div>;
}

With plugins

import { useRef } from 'react';
import { useDraggable, axis, grid } from '@neodrag/react';

function App() {
  const draggableRef = useRef<HTMLDivElement>(null);
  useDraggable(draggableRef, [axis('x'), grid([10, 10])]);

  return <div ref={draggableRef}>Hello</div>;
}

Defining plugins elsewhere with TypeScript

import { useRef } from 'react';
import {
  useDraggable,
  axis,
  bounds,
  BoundsFrom,
  type Plugin,
} from '@neodrag/react';

function App() {
  const draggableRef = useRef<HTMLDivElement>(null);

  const plugins: Plugin[] = [axis('y'), bounds(BoundsFrom.parent())];
  useDraggable(draggableRef, plugins);

  return <div ref={draggableRef}>Hello</div>;
}

Getting drag state

import { useRef, useEffect } from 'react';
import { useDraggable } from '@neodrag/react';

function App() {
  const draggableRef = useRef<HTMLDivElement>(null);
  const dragState = useDraggable(draggableRef);

  useEffect(() => {
    console.log('Position:', dragState.offset);
    console.log('Is dragging:', dragState.isDragging);
  }, [dragState]);

  return <div ref={draggableRef}>Hello</div>;
}

Reactive Plugins with useCompartment

For dynamic behavior that changes during runtime:

import { useRef, useState } from 'react';
import { useDraggable, axis, useCompartment } from '@neodrag/react';

function App() {
  const elementRef = useRef<HTMLDivElement>(null);
  const [currentAxis, setCurrentAxis] = useState<'x' | 'y'>('x');

  const axisCompartment = useCompartment(
    () => axis(currentAxis),
    [currentAxis],
  );

  useDraggable(elementRef, () => [axisCompartment]);

  return (
    <div>
      <div ref={elementRef}>Current axis: {currentAxis}</div>
      <button
        onClick={() =>
          setCurrentAxis(currentAxis === 'x' ? 'y' : 'x')
        }
      >
        Switch Axis
      </button>
    </div>
  );
}

Multiple reactive compartments

import { useRef, useState } from 'react';
import {
  useDraggable,
  axis,
  bounds,
  BoundsFrom,
  grid,
  useCompartment,
} from '@neodrag/react';

function App() {
  const elementRef = useRef<HTMLDivElement>(null);
  const [currentAxis, setCurrentAxis] = useState<'x' | 'y'>('x');
  const [gridSize, setGridSize] = useState(20);
  const [enableBounds, setEnableBounds] = useState(false);

  const axisComp = useCompartment(
    () => axis(currentAxis),
    [currentAxis],
  );
  const gridComp = useCompartment(
    () => grid([gridSize, gridSize]),
    [gridSize],
  );
  const boundsComp = useCompartment(
    () => (enableBounds ? bounds(BoundsFrom.parent()) : null),
    [enableBounds],
  );

  useDraggable(elementRef, () => [axisComp, gridComp, boundsComp]);

  return (
    <div>
      <div ref={elementRef}>Reactive draggable</div>

      <div>
        <select
          value={currentAxis}
          onChange={(e) =>
            setCurrentAxis(e.target.value as 'x' | 'y')
          }
        >
          <option value="x">X</option>
          <option value="y">Y</option>
        </select>

        <input
          type="range"
          min="10"
          max="50"
          value={gridSize}
          onChange={(e) => setGridSize(Number(e.target.value))}
        />

        <label>
          <input
            type="checkbox"
            checked={enableBounds}
            onChange={(e) => setEnableBounds(e.target.checked)}
          />
          Enable bounds
        </label>
      </div>
    </div>
  );
}

Event Handling

import { useRef } from 'react';
import { useDraggable, events } from '@neodrag/react';

function App() {
  const elementRef = useRef<HTMLDivElement>(null);

  useDraggable(elementRef, [
    events({
      onDragStart: (data) => console.log('Started:', data.offset),
      onDrag: (data) => console.log('Dragging:', data.offset),
      onDragEnd: (data) => console.log('Ended:', data.offset),
    }),
  ]);

  return <div ref={elementRef}>Check console while dragging</div>;
}

Drag Controls

import { useRef } from 'react';
import { useDraggable, controls, ControlFrom } from '@neodrag/react';

function App() {
  const elementRef = useRef<HTMLDivElement>(null);

  useDraggable(elementRef, [
    controls({
      allow: ControlFrom.selector('.drag-handle'),
      block: ControlFrom.selector('.no-drag'),
    }),
  ]);

  return (
    <div ref={elementRef}>
      <div className="drag-handle">🔸 Drag from here</div>
      <div>Content area</div>
      <div className="no-drag">❌ Can't drag from here</div>
    </div>
  );
}