537

useDraggable

Hook that makes an element draggable

elementslowtest coverage
Project Atlas
Design reviewIn progress

Finalize the dashboard mockups and hand off specs to engineering before Friday.

import { useDraggable } from '@siberiacancode/reactuse';
import { GripVerticalIcon } from 'lucide-react';
import { useRef } from 'react';

const Demo = () => {
  const cardRef = useRef<HTMLDivElement>(null);

  useDraggable(cardRef, {
    onStart: ({ event }) => {
      const target = event.target as HTMLElement;
      if (!target.closest('[data-drag-handle]')) return false;
      if (cardRef.current) cardRef.current.style.transition = 'none';
    },
    onMove: ({ delta }) => {
      if (cardRef.current)
        cardRef.current.style.transform = `translate(${delta.x}px, ${delta.y}px)`;
    },
    onEnd: () => {
      if (cardRef.current) {
        cardRef.current.style.transition = 'transform 0.35s cubic-bezier(0.22, 1, 0.36, 1)';
        cardRef.current.style.transform = 'translate(0px, 0px)';
      }
    }
  });

  return (
    <section className='relative'>
      <div ref={cardRef} className='w-64 select-none' style={{ transform: 'translate(0px, 0px)' }}>
        <div className='bg-card text-card-foreground border-border rounded-xl border shadow-md'>
          <div
            data-drag-handle
            className='border-border bg-muted/50 flex cursor-grab items-center gap-2 rounded-t-xl border-b px-3 py-2 active:cursor-grabbing'
          >
            <GripVerticalIcon className='text-muted-foreground size-4' />
            <span className='text-sm font-medium'>Project Atlas</span>
          </div>
          <div className='flex flex-col gap-3 p-4'>
            <div className='flex items-center justify-between'>
              <span className='text-sm font-medium'>Design review</span>
              <span className='border-border rounded-full border px-2 py-0.5 text-xs font-medium'>
                In progress
              </span>
            </div>
            <p className='text-muted-foreground text-sm'>
              Finalize the dashboard mockups and hand off specs to engineering before Friday.
            </p>
          </div>
        </div>
      </div>
    </section>
  );
};

export default Demo;

Installation

pnpm add @siberiacancode/reactuse

Usage

const { snapshot, watch, dragging, set } = useDraggable(ref);
// or
const { ref, snapshot, watch, dragging, set } = useDraggable<HTMLDivElement>();

Type Declarations

import type { HookTarget } from '@/utils/helpers';

import type { StateRef } from '../useRefState/useRefState';

export interface UseDraggablePosition {
  /** The x coordinate */
  x: number;
  /** The y coordinate */
  y: number;
}

export interface UseDraggableEvent {
  /** The delta offset from the drag start point (suitable for transform: translate) */
  delta: UseDraggablePosition;
  /** The original pointer event */
  event: PointerEvent;
  /** The absolute position (suitable for left/top with fixed positioning) */
  position: UseDraggablePosition;
}

export interface UseDraggableOptions {
  /** The axis to drag on */
  axis?: 'both' | 'x' | 'y';
  /** The enabled state of the draggable */
  enabled?: boolean;
  /** The initial position of the element */
  initialValue?: UseDraggablePosition;
  /** The callback when dragging ends */
  onEnd?: (params: UseDraggableEvent) => void;
  /** The callback during dragging */
  onMove?: (params: UseDraggableEvent) => void;
  /** The callback when dragging starts. Return `false` to prevent dragging */
  onStart?: (params: UseDraggableEvent) => false | void;
}

export interface UseDraggableReturn {
  /** Whether the element is currently being dragged */
  dragging: boolean;
  /** The latest position snapshot */
  snapshot: UseDraggablePosition;
  /** Function to set the position programmatically */
  set: (position: UseDraggablePosition) => void;
  /** Function to enable subscriptions and rerender on next updates */
  watch: () => UseDraggablePosition;
}

export interface UseDraggable {
  (target: HookTarget, options?: UseDraggableOptions): UseDraggableReturn;

  <Target extends Element>(
    options?: UseDraggableOptions,
    target?: never
  ): UseDraggableReturn & {
    ref: StateRef<Target>;
  };
}

API

Parameters

NameTypeDefaultNote
targetHookTarget-The target element to make draggable
options.enabledbooleantrueThe enabled state of the draggable
options.axis'x' | 'y' | 'both''both'The axis to drag on
options.initialValueUseDraggablePosition-The initial position of the element

Returns

UseDraggableReturn

Parameters

NameTypeDefaultNote
options.enabledbooleantrueThe enabled state of the draggable
options.axis'x' | 'y' | 'both''both'The axis to drag on
options.initialValueUseDraggablePosition-The initial position of the element

Returns

UseDraggableReturn & { ref: StateRef<Target> }

Contributors

ddebabin

Last updated on