Skip to content

zheksoon/snapdrag

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

67 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Snapdrag

⚡️ Simple yet powerful drag-and-drop for React and Vanilla JS ⚡️

Snapdrag in action

What is Snapdrag?

Snapdrag is an alternative vision of how drag-and-drop should be done in React - simple, intuitive, and performant. With just two hooks and an overlay component you can build rich drag-and-drop interactions - starting from simple squares, ending with scrollable and sortable multi-lists.

Snapdrag is built on top of snapdrag/core, a universal building block that works with any framework or vanilla JavaScript.

Key Features

  • 🚀 Minimal, modern API: just two hooks and one overlay component
  • 🎛️ Full control: granular event callbacks for every drag stage
  • 🔄 Two-way data flow: draggables and droppables exchange data seamlessly
  • 🗂️ Multiple drop targets: supports overlapping and nested zones
  • 🔌 Plugins system: easily extend functionality
  • 🛑 No HTML5 DnD: consistent, reliable behavior across browsers
  • ⚡️ Built for performance and extensibility

TL;DR

import { useDraggable, useDroppable, Overlay } from "snapdrag";
import "./styles.css";

const App = () => {
  const { draggable } = useDraggable({
    kind: "SQUARE",
    data: { color: "red" },
    move: true,
  });

  const { droppable } = useDroppable({
    accepts: "SQUARE",
    onDrop({ data }) {
      alert(`Dropped ${data.color} square`);
    },
  });

  return (
    <div className="app">
      <div className="absolute left-100">
        {draggable(<div className="square red">Drag me</div>)}
      </div>
      <div className="absolute left-300">
        {droppable(<div className="square green">Drop on me</div>)}
      </div>
      <Overlay />
    </div>
  );
};

Result:

TL;DR example

Table of Contents

Installation

# npm
npm install --save snapdrag

# yarn
yarn add snapdrag

Basic Concepts

Snapdrag is built around three core components:

  • useDraggable - A hook that makes any React element draggable
  • useDroppable - A hook that makes any React element a potential drop target
  • Overlay - A component that renders the dragged element during drag operations

The fundamental relationship works like this:

  1. Each draggable has a kind (like "CARD" or "ITEM") that identifies what type of element it is
  2. Each droppable specifies what kind it accepts through its configuration
  3. They exchange data during interactions, allowing for rich behaviors and communication

When a draggable is over a compatible droppable, they can exchange information. This unlocks dynamic behaviors such as highlighting, sorting, or visually transforming elements based on the ongoing interaction.

Quick Start Example

Here is more comprehensive example that demonstrate the lifecycle of draggable and droppable items. Usually you need to use only subset of that, but we will show almost every callback for clarity.

Simple drag-and-drop squares

DraggableSquare.tsx

import { useState } from "react";
import { useDraggable } from "snapdrag";

export const DraggableSquare = ({ color }: { color: string }) => {
  const [text, setText] = useState("Drag me");
  const { draggable, isDragging } = useDraggable({
    kind: "SQUARE",
    data: { color },
    move: true,
    // Callbacks are totally optional
    onDragStart({ data }) {
      // data is the own data of the draggable
      setText(`Dragging ${data.color}`);
    },
    onDragMove({ dropTargets }) {
      // Check if there are any drop targets under the pointer
      if (dropTargets.length > 0) {
        // Update the text based on the first drop target color
        setText(`Over ${dropTargets[0].data.color}`);
      } else {
        setText("Dragging...");
      }
    },
    onDragEnd({ dropTargets }) {
      // Check if the draggable was dropped on a valid target
      if (dropTargets.length > 0) {
        setText(`Dropped on ${dropTargets[0].data.color}`);
      } else {
        setText("Drag me");
      }
    },
  });

  const opacity = isDragging ? 0.5 : 1;

  return draggable(
    <div className="square draggable" style={{ backgroundColor: color, opacity }}>
      {text}
    </div>
  );
};

DroppableSquare.tsx

import { useState } from "react";
import { useDroppable } from "snapdrag";

export const DroppableSquare = ({ color }: { color: string }) => {
  const [text, setText] = useState("Drop here");

  const { droppable } = useDroppable({
    accepts: "SQUARE",
    data: { color },
    // Optional callbacks
    onDragIn({ data }) {
      // Some draggable is hovering over this droppable
      // data is the data of the draggable
      setText(`Hovered over ${data.color}`);
    },
    onDragOut() {
      // The draggable is no longer hovering over this droppable
      setText("Drop here");
    },
    onDrop({ data }) {
      // Finally, the draggable is dropped on this droppable
      setText(`Dropped ${data.color}`);
    },
  });

  return droppable(
    <div className="square droppable" style={{ backgroundColor: color }}>
      {text}
    </div>
  );
};

App.tsx

import { Overlay } from "snapdrag";

export default function App() {
  return (
    <div className="app relative">
      {/* Just two squares for simplicity */}
      <div className="absolute top-100 left-100">
        <DraggableSquare color="red" />
      </div>
      <div className="absolute top-100 left-300">
        <DroppableSquare color="green" />
      </div>

      {/* Render overlay to show the dragged component */}
      <Overlay />
    </div>
  );
}

This example on CodeSandbox

How Snapdrag Works

Under the hood, Snapdrag takes a different approach than traditional drag-and-drop libraries:

  1. Event Listening: Snapdrag attaches a pointerdown event listener to draggable elements
  2. Tracking Movement: Once triggered, it tracks pointermove events on the document until pointerup occurs
  3. Finding Targets: On every move, it uses document.elementsFromPoint() to check what elements are under the cursor
  4. Target Handling: It then determines which droppable elements are valid targets and manages the interaction
  5. Event Firing: Appropriate callbacks are fired based on the current state of the drag operation

Unlike HTML5 drag-and-drop which has limited customization options, Snapdrag gives you control over every aspect of the drag experience.

You can change settings of draggable and droppable at any time during the drag operation, making Snapdrag extremely flexible. Want to dynamically change what a draggable can do based on its current position? No problem!

Core Components

useDraggable

The useDraggable hook makes any React element draggable. It returns an object with two properties:

  • draggable: A function that wraps your component, making it draggable
  • isDragging: A boolean indicating if the element is currently being dragged

Basic usage:

const DraggableItem = () => {
  const { draggable, isDragging } = useDraggable({
    kind: "ITEM", // Required: identifies this draggable type
    data: { id: "123" }, // Optional: data to share during drag operations
    move: true, // Optional: move vs clone during dragging
  });

  return draggable(<div className={isDragging ? "dragging" : ""}>Drag me!</div>);
};

Important Note: The wrapped component must accept a ref to the DOM node to be draggable. If you already have a ref, Snapdrag will handle it correctly:

const myRef = useRef(null);

const { draggable } = useDraggable({
  kind: "ITEM",
});

// Both refs work correctly
return draggable(<div ref={myRef} />);

You can even make an element both draggable and droppable:

const { draggable } = useDraggable({ kind: "ITEM" });
const { droppable } = useDroppable({ accepts: "ITEM" });

// Combine the wrappers (order doesn't matter)
return draggable(droppable(<div>I'm both!</div>));

useDroppable

The useDroppable hook makes any React element a potential drop target. It returns:

  • droppable: A function that wraps your component, making it a drop target
  • hovered: Data about the draggable currently hovering over this element (or null if none)

Basic usage:

const DropZone = () => {
  const { droppable, hovered } = useDroppable({
    accepts: "ITEM", // Required: which draggable kinds to accept
    data: { zone: "main" }, // Optional: data to share with draggables
    onDrop({ data }) {
      // Optional: handle successful drops
      console.log("Dropped item:", data.id);
    },
  });

  // Change appearance when being hovered
  const isHovered = Boolean(hovered);

  return droppable(<div className={isHovered ? "drop-zone hovered" : "drop-zone"}>Drop here</div>);
};

Overlay

The Overlay component renders the currently dragged element. It should be included once in your application:

import { Overlay } from "snapdrag";

function App() {
  return (
    <div>
      {/* Your app content */}
      <YourDraggableComponents />

      {/* Required: Shows the dragged element */}
      <Overlay className="your-class" style={someStyles} />
    </div>
  );
}

You can add your own classes and styles to the overlay to make it fit your application.

Draggable Lifecycle

The draggable component goes through a lifecycle during drag interactions, with callbacks at each stage.

onDragStart

Called when the drag operation begins (after the user clicks and begins moving, and after shouldDrag if provided, returns true):

const { draggable } = useDraggable({
  kind: "CARD",
  onDragStart({ data, event, dragStartEvent, element }) {
    console.log("Started dragging card:", data.id);
    // Setup any state needed during dragging
  },
});

The callback receives an object with the following properties:

  • data: The draggable's data (from the data config option of useDraggable).
  • event: The PointerEvent that triggered the drag start (usually the first pointermove after pointerdown and shouldDrag validation).
  • dragStartEvent: The initial PointerEvent from pointerdown that initiated the drag attempt.
  • element: The DOM element that is being dragged (this is the element rendered in the Overlay).

onDragMove

Called on every pointer movement during dragging:

const { draggable } = useDraggable({
  kind: "CARD",
  onDragMove({ dropTargets, top, left, data, event, dragStartEvent, element }) {
    // dropTargets contains info about all drop targets under the pointer
    if (dropTargets.length > 0) {
      console.log("Over drop zone:", dropTargets[0].data.zone);
    }

    // top and left are the screen coordinates of the draggable
    console.log(`Position: ${left}px, ${top}px`);
  },
});

In addition to the properties from onDragStart (data, dragStartEvent, element), this callback receives:

  • event: The current PointerEvent from the pointermove handler.
  • dropTargets: An array of objects, each representing a droppable target currently under the pointer. Each object contains:
    • data: The data associated with the droppable (from its useDroppable configuration).
    • element: The DOM element of the droppable.
  • top: The calculated top screen coordinate of the draggable element in the overlay.
  • left: The calculated left screen coordinate of the draggable element in the overlay.

Note: This callback is called frequently, so avoid expensive operations here.

onDragEnd

Called when the drag operation completes (on pointerup):

const { draggable } = useDraggable({
  kind: "CARD",
  onDragEnd({ dropTargets, top, left, data, event, dragStartEvent, element }) {
    if (dropTargets.length > 0) {
      console.log("Dropped on:", dropTargets[0].data.zone);
    } else {
      console.log("Dropped outside of any drop zone");
      // Handle "cancel" logic
    }
  },
});

Receives the same properties as onDragMove (data, event, dragStartEvent, element, dropTargets, top, left). If the user dropped the element on valid drop targets, dropTargets will contain them; otherwise, it will be an empty array. The top and left coordinates represent the final position of the draggable in the overlay just before it's hidden.

Droppable Lifecycle

The droppable component also has lifecycle events during drag interactions. All droppable callbacks receive a dropTargets array, similar to the one in useDraggable's onDragMove and onDragEnd, representing all droppables currently under the pointer.

onDragIn

Called when a draggable first enters this drop target:

const { droppable } = useDroppable({
  accepts: "CARD",
  onDragIn({ kind, data, event, element, dropElement, dropTargets }) {
    console.log(`${kind} entered drop zone`);
    // Change appearance, update state, etc.
  },
});

The callback receives an object with:

  • kind: The kind of the draggable that entered.
  • data: The data from the draggable.
  • event: The current PointerEvent from the pointermove handler.
  • element: The DOM element of the draggable.
  • dropElement: The DOM element of this droppable.
  • dropTargets: Array of all active drop targets under the pointer, including the current one. Each entry contains:
    • data: The data from the droppable (from its useDroppable configuration).
    • element: The DOM element of the droppable.

This is called once when a draggable enters and can be used to trigger animations or state changes.

onDragMove (Droppable)

Called as a draggable moves within the drop target:

const { droppable } = useDroppable({
  accepts: "CARD",
  onDragMove({ kind, data, event, element, dropElement, dropTargets }) {
    // Calculate position within the drop zone
    const rect = dropElement.getBoundingClientRect();
    const x = event.clientX - rect.left;
    const y = event.clientY - rect.top;

    console.log(`Position in drop zone: ${x}px, ${y}px`);
  },
});

Receives the same properties as onDragIn. Like the draggable version, this is called frequently, so keep operations light. This is perfect for creating dynamic visual cues like highlighting different sections of your drop zone based on cursor position.

onDragOut

Called when a draggable leaves the drop target:

const { droppable } = useDroppable({
  accepts: "CARD",
  onDragOut({ kind, data, event, element, dropElement, dropTargets }) {
    console.log(`${kind} left drop zone`);
    // Revert animations, update state, etc.
  },
});

Receives the same properties as onDragIn. This is typically used to undo changes made in onDragIn. Use it to clean up and reset any visual changes you made when the draggable entered.

onDrop

Called when a draggable is successfully dropped on this target:

const { droppable } = useDroppable({
  accepts: "CARD",
  onDrop({ kind, data, event, element, dropElement, dropTargets }) {
    console.log(`${kind} was dropped with data:`, data);
    // Handle the dropped item
  },
});

Receives the same properties as onDragIn. This is where you implement the main logic for what happens when a drop succeeds. Update your application state, save the new position, or trigger any other business logic related to the completed drag operation.

Common Patterns

Two-way Data Exchange

Snapdrag makes it simple for draggables and droppables to talk to each other by exchanging data in both directions:

// Draggable component accessing droppable data
const { draggable } = useDraggable({
  kind: "CARD",
  data: { id: "card-1", color: "red" },
  onDragMove({ dropTargets }) {
    if (dropTargets.length > 0) {
      // Read data from the drop zone underneath
      const dropZoneType = dropTargets[0].data.type;
      console.log(`Over ${dropZoneType} zone`);
    }
  },
});

// Droppable component accessing draggable data
const { droppable, hovered } = useDroppable({
  accepts: "CARD",
  data: { type: "inbox" },
  onDragIn({ data }) {
    console.log(`Card ${data.id} entered inbox`);
  },
});

This pattern is especially useful for adapting the UI based on the interaction context.

Dynamic Colors Example

Here's how to create a draggable that changes color based on the droppable it's over:

// In DraggableSquare.tsx
import { useState } from "react";
import { useDraggable } from "snapdrag";

export const DraggableSquare = ({ color: initialColor }) => {
  const [color, setColor] = useState(initialColor);

  const { draggable, isDragging } = useDraggable({
    kind: "SQUARE",
    data: { color },
    move: true,
    onDragMove({ dropTargets }) {
      if (dropTargets.length) {
        setColor(dropTargets[0].data.color);
      } else {
        setColor(initialColor);
      }
    },
    onDragEnd() {
      setColor(initialColor); // Reset on drop
    },
  });

  return draggable(
    <div
      className="square"
      style={{
        backgroundColor: color,
        opacity: isDragging ? 0.9 : 1,
      }}
    >
      {isDragging ? "Dragging" : "Drag me"}
    </div>
  );
};

// In DroppableSquare.tsx
import { useDroppable } from "snapdrag";

export const DroppableSquare = ({ color }) => {
  const [text, setText] = useState("Drop here");

  const { droppable } = useDroppable({
    accepts: "SQUARE",
    data: { color }, // Share this color with draggables
    onDrop({ data }) {
      setText(`Dropped ${data.color}`);
    },
  });

  return droppable(
    <div className="square" style={{ backgroundColor: color }}>
      {text}
    </div>
  );
};

Dynamic Border Example

This example shows how to create a visual indication of where an item will be dropped:

import { useState } from "react";
import { useDroppable } from "snapdrag";

export const DroppableSquare = ({ color }) => {
  const [text, setText] = useState("Drop here");
  const [borderPosition, setBorderPosition] = useState("");

  const { droppable } = useDroppable({
    accepts: "SQUARE",
    onDragMove({ event, dropElement }) {
      // Calculate which quadrant of the square the pointer is in
      const { top, left, height } = dropElement.getBoundingClientRect();
      const x = event.clientX - left;
      const y = event.clientY - top;

      // Set border on the appropriate side
      if (x / y < 1.0) {
        if (x / (height - y) < 1.0) {
          setBorderPosition("borderLeft");
        } else {
          setBorderPosition("borderBottom");
        }
      } else {
        if (x / (height - y) < 1.0) {
          setBorderPosition("borderTop");
        } else {
          setBorderPosition("borderRight");
        }
      }
    },
    onDragOut() {
      setBorderPosition(""); // Remove border when draggable leaves
    },
    onDrop({ data }) {
      setText(`Dropped ${data.color}`);
      setBorderPosition(""); // Remove border after drop
    },
  });

  // Add border to appropriate side
  const style = {
    backgroundColor: color,
    [borderPosition]: "10px solid red",
  };

  return droppable(
    <div className="square" style={style}>
      {text}
    </div>
  );
};

Multiple Drop Targets

Snapdrag handles the case where multiple drop targets overlap:

const { draggable } = useDraggable({
  kind: "ITEM",
  onDragMove({ dropTargets }) {
    // Sort by order to find the topmost
    const sorted = [...dropTargets].sort((a, b) => b.data.order - a.data.order);

    if (sorted.length) {
      console.log(`Topmost target: ${sorted[0].data.name}`);
    }
  },
});

// ... somewere in your code

const { droppable } = useDroppable({
  accepts: "ITEM",
  data: { order: 10 },
});

You also can you DOM elements to get the topmost drop target:

const { draggable } = useDraggable({
  kind: "ITEM",
  onDragMove({ dropTargets }) {
    // Sort by order to find the topmost

    const sorted = [...dropTargets].sort((a, b) => {
      // access drop target element instead of data
      const aIndex = a.element.getComputedStyle().zIndex || 0;
      const bIndex = b.element.getComputedStyle().zIndex || 0;

      return bIndex - aIndex;
    });

    if (sorted.length) {
      console.log(`Topmost target: ${sorted[0].data.name}`);
    }
  },
});

Drag Threshold

For finer control, you can start dragging only after the pointer has moved a certain distance:

const { draggable } = useDraggable({
  kind: "ITEM",
  shouldDrag({ event, dragStartEvent }) {
    // Calculate distance from start position
    const dx = event.clientX - dragStartEvent.clientX;
    const dy = event.clientY - dragStartEvent.clientY;
    const distance = Math.sqrt(dx * dx + dy * dy);

    // Only start dragging after moving 5px
    return distance > 5;
  },
});

Touch Support

Snapdrag supports touch events out of the box. It uses PointerEvent to handle both mouse and touch interactions seamlessly. You can use the same API for both types of events.

To make your draggable elements touch-friendly, ensure they are touchable (e.g., using touch-action: none in CSS). The container can have touch-action: pan-x or touch-action: pan-y to allow scrolling while dragging.

Examples

Snapdrag includes several examples that demonstrate its capabilities, from simple to complex use cases.

Basic: Colored Squares

The simplest example shows dragging a colored square onto a drop target:

Simple squares

This demonstrates the fundamentals of drag-and-drop with Snapdrag:

  • Defining a draggable with kind and data
  • Creating a drop target that accepts the draggable
  • Handling the onDrop event

Try it on CodeSandbox

Intermediate: Simple List

A sortable list where items can be reordered by dragging:

Simple List

This example demonstrates:

  • Using data to identify list items
  • Visual feedback during dragging (blue insertion line)
  • Reordering items in a state array on drop

Try it on CodeSandbox

Advanced: List with Animations

A more sophisticated list with smooth animations:

Advanced list

This example showcases:

  • CSS transitions for smooth animations
  • A special drop area for appending items to the end
  • Animated placeholders that create space for dropped items

Try it on CodeSandbox

Expert: Kanban Board

A full kanban board with multiple columns and draggable cards:

Kanban Board

This complex example demonstrates advanced features:

  • Multiple drop targets with different behaviors
  • Conditional acceptance of draggables
  • Smooth animations during drag operations
  • Two-way data exchange between components
  • Touch support with drag threshold
  • Item addition and removal

All this is achieved in just about 200 lines of code (excluding state management and styling).

Try it on CodeSandbox

API Reference

useDraggable Configuration

The useDraggable hook accepts a configuration object with these options:

Option Type Description
kind string or symbol Required. Identifies this draggable type
data object or function Data to share with droppables. Can be a static object or a function that returns data
disabled boolean When true, disables dragging functionality
move boolean When true, moves the component instead of cloning it to the overlay
component function Provides a custom component to show while dragging
placeholder function Custom component to show in place of the dragged item
offset object or function Controls positioning relative to cursor

Event Callbacks:

Callback Description
shouldDrag Function determining if dragging should start. Must return true or false
onDragStart Called when drag begins
onDragMove Called on every pointer move while dragging
onDragEnd Called when dragging ends

Detailed Configuration Description

kind (Required)

Defines the type of the draggable. It must be a unique string or symbol.

const { draggable } = useDraggable({
  kind: "SQUARE", // Identify this as a "SQUARE" type
});
data

Data associated with the draggable. It can be a static object or a function that returns an object:

// Static object
const { draggable } = useDraggable({
  kind: "SQUARE",
  data: { color: "red", id: "square-1" },
});

// Function (calculated at drag start)
const { draggable } = useDraggable({
  kind: "SQUARE",
  data: ({ dragElement, dragStartEvent }) => ({
    id: dragElement.id,
    position: { x: dragStartEvent.clientX, y: dragStartEvent.clientY },
  }),
});
disabled

When true, temporarily disables dragging:

const { draggable } = useDraggable({
  kind: "SQUARE",
  disabled: !canDrag, // Disable based on some condition
});
move

When true, the original component is moved during dragging instead of creating a clone:

const { draggable } = useDraggable({
  kind: "SQUARE",
  move: true, // Move the actual component
});

Note: If move is false (default), the component is cloned to the overlay layer while the original stays in place. The original component won't receive prop updates during dragging.

component

A function that returns a custom component to be shown during dragging:

const { draggable } = useDraggable({
  kind: "SQUARE",
  component: ({ data, props }) => <Square color={data.color} style={{ opacity: 0.8 }} />,
});
placeholder

A function that returns a component to be shown in place of the dragged item:

const { draggable } = useDraggable({
  kind: "SQUARE",
  placeholder: ({ data, props }) => <Square color="gray" style={{ opacity: 0.4 }} />,
});

When specified, the move option is ignored.

offset

Controls the offset of the dragging component relative to the cursor:

// Static offset
const { draggable } = useDraggable({
  kind: "SQUARE",
  offset: { top: 10, left: 10 }, // 10px down and right from cursor
});

// Dynamic offset
const { draggable } = useDraggable({
  kind: "SQUARE",
  offset: ({ element, event, data }) => {
    // Calculate based on event or element position
    return { top: 0, left: 0 };
  },
});

If not specified, the offset is calculated to maintain the element's initial position relative to the cursor.

Callback Details

shouldDrag

Function that determines if dragging should start. It's called on every pointer move until it returns true or the drag attempt ends:

const { draggable } = useDraggable({
  kind: "SQUARE",
  shouldDrag: ({ event, dragStartEvent, element, data }) => {
    // Only drag if shifted 10px horizontally
    return Math.abs(event.clientX - dragStartEvent.clientX) > 10;
  },
});
onDragStart

Called when dragging begins (after shouldDrag returns true):

const { draggable } = useDraggable({
  kind: "SQUARE",
  onDragStart: ({ event, dragStartEvent, element, data }) => {
    console.log("Drag started at:", event.clientX, event.clientY);
    // Setup any initial state needed during drag
  },
});
onDragMove

Called on every pointer move during dragging:

const { draggable } = useDraggable({
  kind: "SQUARE",
  onDragMove: ({ event, dragStartEvent, element, data, dropTargets, top, left }) => {
    // Current drop targets under the pointer
    if (dropTargets.length) {
      console.log("Over drop zone:", dropTargets[0].data.name);
    }

    // Current position of the draggable
    console.log("Position:", top, left);
  },
});

The dropTargets array contains information about all current drop targets under the cursor. Each entry has data (from the droppable's configuration) and element (the DOM element).

onDragEnd

Called when dragging ends:

const { draggable } = useDraggable({
  kind: "SQUARE",
  onDragEnd: ({ event, dragStartEvent, element, data, dropTargets }) => {
    if (dropTargets.length) {
      console.log("Dropped on:", dropTargets[0].data.name);
    } else {
      console.log("Dropped outside any drop target");
      // Handle "cancel" case
    }
  },
});

useDroppable Configuration

The useDroppable hook accepts a configuration object with these options:

Option Type Description
accepts string, symbol, array, or function Required. What draggable kinds to accept
data object Data to share with draggables
disabled boolean When true, disables dropping

Event Callbacks:

Callback Description
onDragIn Called when a draggable enters this droppable
onDragOut Called when a draggable leaves this droppable
onDragMove Called when a draggable moves within this droppable
onDrop Called when a draggable is dropped on this droppable

Detailed Configuration Description

accepts (Required)

Defines what kinds of draggables this drop target can accept:

// Accept a single kind
const { droppable } = useDroppable({
  accepts: "SQUARE",
});

// Accept multiple kinds
const { droppable } = useDroppable({
  accepts: ["SQUARE", "CIRCLE"],
});

// Use a function for more complex logic
const { droppable } = useDroppable({
  accepts: ({ kind, data }) => {
    // Check both kind and data to determine acceptance
    return kind === "SQUARE" && data.color === "red";
  },
});
data

Data associated with the droppable area:

const { droppable } = useDroppable({
  accepts: "SQUARE",
  data: {
    zoneId: "dropzone-1",
    capacity: 5,
    color: "blue",
  },
});

This data is accessible to draggables through the dropTargets array in their callbacks.

disabled

When true, temporarily disables dropping:

const { droppable } = useDroppable({
  accepts: "SQUARE",
  disabled: isFull, // Disable based on some condition
});

Callback Details

onDragIn

Called when a draggable of an accepted kind first enters this drop target:

const { droppable } = useDroppable({
  accepts: "SQUARE",
  onDragIn: ({ kind, data, event, element, dropElement, dropTargets }) => {
    console.log(`${kind} entered with data:`, data);
    // Change appearance, play sound, etc.
  },
});

Arguments:

  • kind - The kind of the draggable
  • data - The data from the draggable
  • event - The current pointer event
  • element - The draggable element
  • dropElement - The droppable element
  • dropTargets - Array of all current drop targets under the pointer
onDragOut

Called when a draggable leaves this drop target:

const { droppable } = useDroppable({
  accepts: "SQUARE",
  onDragOut: ({ kind, data, event, element, dropElement, dropTargets }) => {
    console.log(`${kind} left the drop zone`);
    // Revert appearance changes, etc.
  },
});

Arguments are the same as onDragIn.

onDragMove

Called when a draggable moves within this drop target:

const { droppable } = useDroppable({
  accepts: "SQUARE",
  onDragMove: ({ kind, data, event, element, dropElement, dropTargets }) => {
    // Calculate position within drop zone
    const rect = dropElement.getBoundingClientRect();
    const relativeX = event.clientX - rect.left;
    const relativeY = event.clientY - rect.top;

    console.log(`Position in zone: ${relativeX}px, ${relativeY}px`);
  },
});

Arguments are the same as onDragIn.

onDrop

Called when a draggable is dropped on this target:

const { droppable } = useDroppable({
  accepts: "SQUARE",
  onDrop: ({ kind, data, event, element, dropElement, dropTargets }) => {
    console.log(`${kind} was dropped with data:`, data);
    // Handle the dropped item (update state, etc.)
  },
});

Arguments are the same as the other callbacks.

Plugins

Snapdrag offers a plugin system to extend its core functionality. Plugins can hook into the draggable lifecycle events (onDragStart, onDragMove, onDragEnd) to add custom behaviors.

Scroller Plugin

The scroller plugin automatically scrolls a container element when a dragged item approaches its edges. This is useful for large scrollable areas where users might need to drag items beyond the visible viewport.

Initialization

To use the scroller plugin, first create an instance of it by calling createScroller(config).

import { createScroller } from "snapdrag/plugins";

const scroller = createScroller({
  x: true, // Enable horizontal scrolling with default settings
  y: { threshold: 150, speed: 1000, distancePower: 2 }, // Enable vertical scrolling with custom settings
});

Configuration Options (ScrollerConfig)

  • x: (Optional) Enables or configures horizontal scrolling.
    • boolean: If true, uses default settings. If false or omitted, horizontal scrolling is disabled.
    • object (AxisConfig): Allows fine-tuning of horizontal scrolling behavior:
      • threshold (number, default: 100): The distance in pixels from the container's edge at which scrolling should begin.
      • speed (number, default: 2000): The maximum scroll speed in pixels per second when the pointer is at the very edge of the container.
      • distancePower (number, default: 1.5): Controls the acceleration of scrolling as the pointer gets closer to the edge. A higher value means faster acceleration.
  • y: (Optional) Enables or configures vertical scrolling. Accepts the same boolean or object (AxisConfig) values as x.

Usage with useDraggable

Once created, the scroller instance needs to be passed to the plugins array in the useDraggable hook's configuration. The scroller function itself takes the scrollable container element as an argument.

import { useDraggable } from "snapdrag";
import { createScroller } from "snapdrag/plugins";
import { useRef, useEffect, useState } from "react";

// Initialize the scroller plugin
const scrollerPlugin = createScroller({ x: true, y: true });

const DraggableComponent = () => {
  // State to hold the container element once it's mounted
  const [scrollContainer, setScrollContainer] = useState(null);

  const { draggable } = useDraggable({
    kind: "ITEM",
    data: { id: "my-item" },
    plugins: [scrollerPlugin(scrollContainer)],
  });

  return (
    <div
      ref={setScrollContainer}
      style={{ overflow: "auto", height: "200px", width: "300px", border: "1px solid black" }}
    >
      <div style={{ height: "500px", width: "800px" }}>
        {/* Inner content larger than container */}
        {draggable(
          <div style={{ width: "100px", height: "50px", backgroundColor: "lightblue" }}>
            Drag me
          </div>
        )}
        {/* More draggable items or content here */}
      </div>
    </div>
  );
};

How it Works

  1. Initialization: createScroller returns a new scroller function configured with your desired settings.
  2. Plugin Attachment: When you pass scrollerPlugin(containerElement) to useDraggable, Snapdrag calls the appropriate lifecycle methods of the plugin (onDragStart, onDragMove, onDragEnd).
  3. Drag Monitoring: During a drag operation, onDragMove is continuously called. The scroller plugin checks the pointer's position relative to the specified containerElement.
  4. Edge Detection: If the pointer moves within the threshold distance of an edge for an enabled axis (x or y), the plugin initiates scrolling.
  5. Scrolling Speed: The scrolling speed increases polynomially (based on distancePower) as the pointer gets closer to the edge, up to the maximum speed.
  6. Animation Loop: Scrolling is performed using requestAnimationFrame for smooth animation.
  7. Cleanup: When the drag ends (onDragEnd) or the component unmounts, the plugin cleans up any active animation frames.

Important Considerations:

  • The containerElement passed to the scroller function must be the actual scrollable DOM element.
  • Ensure the containerElement has overflow: auto or overflow: scroll CSS properties set for the respective axes you want to enable scrolling on.
  • If the scrollable container is not immediately available on component mount (e.g., if its ref is populated later), you might need to conditionally apply the plugin or update it, as shown in the example using useState and useEffect to pass the container element once it's available.
  • The plugin calculates distances based on the viewport. If your scroll container or draggable items are scaled using CSS transforms, you might need to adjust threshold and speed values accordingly or ensure pointer events are correctly mapped.

The scroller plugin offers a straightforward way to add automatic scrolling to your drag-and-drop interfaces. It significantly enhances usability, especially when users need to drag items across large, scrollable containers or overflowing content areas.

Browser Compatibility

Snapdrag is compatible with all modern browsers that support Pointer Events. This includes:

  • Chrome 55+
  • Firefox 59+
  • Safari 13.1+
  • Edge 18+

Mobile devices are also supported as long as they support Pointer Events.

License

MIT

Author

Eugene Daragan

About

A simple, lightweight, and performant drag and drop library for React and vanilla JS

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 3

  •  
  •  
  •  

Languages