(config: T) {
404 | return new Draggable(config);
405 | }
406 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | ⚡️ Simple yet powerful drag-and-drop for React and Vanilla JS ⚡️
7 |
8 |
9 |
10 |
11 |
12 |
13 | ## What is Snapdrag?
14 |
15 | **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.
16 |
17 | Snapdrag is built on top of `snapdrag/core`, a universal building block that works with any framework or vanilla JavaScript.
18 |
19 | ## Key Features
20 |
21 | - 🚀 **Minimal, modern API:** just two hooks and one overlay component
22 | - 🎛️ **Full control:** granular event callbacks for every drag stage
23 | - 🔄 **Two-way data flow:** draggables and droppables exchange data seamlessly
24 | - 🗂️ **Multiple drop targets:** supports overlapping and nested zones
25 | - 🔌 **Plugins system:** easily extend functionality
26 | - 🛑 **No HTML5 DnD:** consistent, reliable behavior across browsers
27 | - ⚡️ **Built for performance and extensibility**
28 |
29 | ## TL;DR
30 |
31 | ```tsx
32 | import { useDraggable, useDroppable, Overlay } from "snapdrag";
33 | import "./styles.css";
34 |
35 | const App = () => {
36 | const { draggable } = useDraggable({
37 | kind: "SQUARE",
38 | data: { color: "red" },
39 | move: true,
40 | });
41 |
42 | const { droppable } = useDroppable({
43 | accepts: "SQUARE",
44 | onDrop({ data }) {
45 | alert(`Dropped ${data.color} square`);
46 | },
47 | });
48 |
49 | return (
50 |
51 |
52 | {draggable(
Drag me
)}
53 |
54 |
55 | {droppable(
Drop on me
)}
56 |
57 |
58 |
59 | );
60 | };
61 | ```
62 |
63 | Result:
64 |
65 |
66 |
67 |
68 |
69 | ## Table of Contents
70 |
71 | - [Installation](#installation)
72 | - [Basic Concepts](#basic-concepts)
73 | - [Quick Start Example](#quick-start-example)
74 | - [How Snapdrag Works](#how-snapdrag-works)
75 | - [Core Components](#core-components)
76 | - [useDraggable](#usedraggable)
77 | - [useDroppable](#usedroppable)
78 | - [Overlay](#overlay)
79 | - [Draggable Lifecycle](#draggable-lifecycle)
80 | - [Droppable Lifecycle](#droppable-lifecycle)
81 | - [Common Patterns](#common-patterns)
82 | - [Examples](#examples)
83 | - [Basic: Colored Squares](#basic-colored-squares)
84 | - [Intermediate: Simple List](#intermediate-simple-list)
85 | - [Advanced: List with Animations](#advanced-list-with-animations)
86 | - [Expert: Kanban Board](#expert-kanban-board)
87 | - [API Reference](#api-reference)
88 | - [useDraggable Configuration](#usedraggable-configuration)
89 | - [useDroppable Configuration](#usedroppable-configuration)
90 | - [Plugins](#plugins)
91 | - [Browser Compatibility](#browser-compatibility)
92 | - [License](#license)
93 | - [Author](#author)
94 |
95 | ## Installation
96 |
97 | ```bash
98 | # npm
99 | npm install --save snapdrag
100 |
101 | # yarn
102 | yarn add snapdrag
103 | ```
104 |
105 | ## Basic Concepts
106 |
107 | Snapdrag is built around three core components:
108 |
109 | - **`useDraggable`** - A hook that makes any React element draggable
110 | - **`useDroppable`** - A hook that makes any React element a potential drop target
111 | - **`Overlay`** - A component that renders the dragged element during drag operations
112 |
113 | The fundamental relationship works like this:
114 |
115 | 1. Each draggable has a **`kind`** (like "CARD" or "ITEM") that identifies what type of element it is
116 | 2. Each droppable specifies what **`kind`** it **`accepts`** through its configuration
117 | 3. They exchange **`data`** during interactions, allowing for rich behaviors and communication
118 |
119 | 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.
120 |
121 | ## Quick Start Example
122 |
123 | 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.
124 |
125 |
126 |
127 |
128 |
129 | **DraggableSquare.tsx**
130 |
131 | ```tsx
132 | import { useState } from "react";
133 | import { useDraggable } from "snapdrag";
134 |
135 | export const DraggableSquare = ({ color }: { color: string }) => {
136 | const [text, setText] = useState("Drag me");
137 | const { draggable, isDragging } = useDraggable({
138 | kind: "SQUARE",
139 | data: { color },
140 | move: true,
141 | // Callbacks are totally optional
142 | onDragStart({ data }) {
143 | // data is the own data of the draggable
144 | setText(`Dragging ${data.color}`);
145 | },
146 | onDragMove({ dropTargets }) {
147 | // Check if there are any drop targets under the pointer
148 | if (dropTargets.length > 0) {
149 | // Update the text based on the first drop target color
150 | setText(`Over ${dropTargets[0].data.color}`);
151 | } else {
152 | setText("Dragging...");
153 | }
154 | },
155 | onDragEnd({ dropTargets }) {
156 | // Check if the draggable was dropped on a valid target
157 | if (dropTargets.length > 0) {
158 | setText(`Dropped on ${dropTargets[0].data.color}`);
159 | } else {
160 | setText("Drag me");
161 | }
162 | },
163 | });
164 |
165 | const opacity = isDragging ? 0.5 : 1;
166 |
167 | return draggable(
168 |
169 | {text}
170 |
171 | );
172 | };
173 | ```
174 |
175 | **DroppableSquare.tsx**
176 |
177 | ```tsx
178 | import { useState } from "react";
179 | import { useDroppable } from "snapdrag";
180 |
181 | export const DroppableSquare = ({ color }: { color: string }) => {
182 | const [text, setText] = useState("Drop here");
183 |
184 | const { droppable } = useDroppable({
185 | accepts: "SQUARE",
186 | data: { color },
187 | // Optional callbacks
188 | onDragIn({ data }) {
189 | // Some draggable is hovering over this droppable
190 | // data is the data of the draggable
191 | setText(`Hovered over ${data.color}`);
192 | },
193 | onDragOut() {
194 | // The draggable is no longer hovering over this droppable
195 | setText("Drop here");
196 | },
197 | onDrop({ data }) {
198 | // Finally, the draggable is dropped on this droppable
199 | setText(`Dropped ${data.color}`);
200 | },
201 | });
202 |
203 | return droppable(
204 |
205 | {text}
206 |
207 | );
208 | };
209 | ```
210 |
211 | **App.tsx**
212 |
213 | ```tsx
214 | import { Overlay } from "snapdrag";
215 |
216 | export default function App() {
217 | return (
218 |
219 | {/* Just two squares for simplicity */}
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 | {/* Render overlay to show the dragged component */}
228 |
229 |
230 | );
231 | }
232 | ```
233 |
234 | This example on [CodeSandbox](https://codesandbox.io/p/sandbox/snapdrag-simple-squares-8rw96s)
235 |
236 | ## How Snapdrag Works
237 |
238 | Under the hood, Snapdrag takes a different approach than traditional drag-and-drop libraries:
239 |
240 | 1. **Event Listening**: Snapdrag attaches a `pointerdown` event listener to draggable elements
241 | 2. **Tracking Movement**: Once triggered, it tracks `pointermove` events on the document until `pointerup` occurs
242 | 3. **Finding Targets**: On every move, it uses `document.elementsFromPoint()` to check what elements are under the cursor
243 | 4. **Target Handling**: It then determines which droppable elements are valid targets and manages the interaction
244 | 5. **Event Firing**: Appropriate callbacks are fired based on the current state of the drag operation
245 |
246 | Unlike HTML5 drag-and-drop which has limited customization options, Snapdrag gives you control over every aspect of the drag experience.
247 |
248 | 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!
249 |
250 | ## Core Components
251 |
252 | ### `useDraggable`
253 |
254 | The `useDraggable` hook makes any React element draggable. It returns an object with two properties:
255 |
256 | - `draggable`: A function that wraps your component, making it draggable
257 | - `isDragging`: A boolean indicating if the element is currently being dragged
258 |
259 | Basic usage:
260 |
261 | ```tsx
262 | const DraggableItem = () => {
263 | const { draggable, isDragging } = useDraggable({
264 | kind: "ITEM", // Required: identifies this draggable type
265 | data: { id: "123" }, // Optional: data to share during drag operations
266 | move: true, // Optional: move vs clone during dragging
267 | });
268 |
269 | return draggable(Drag me!
);
270 | };
271 | ```
272 |
273 | **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:
274 |
275 | ```jsx
276 | const myRef = useRef(null);
277 |
278 | const { draggable } = useDraggable({
279 | kind: "ITEM",
280 | });
281 |
282 | // Both refs work correctly
283 | return draggable();
284 | ```
285 |
286 | You can even make an element both draggable and droppable:
287 |
288 | ```jsx
289 | const { draggable } = useDraggable({ kind: "ITEM" });
290 | const { droppable } = useDroppable({ accepts: "ITEM" });
291 |
292 | // Combine the wrappers (order doesn't matter)
293 | return draggable(droppable(I'm both!
));
294 | ```
295 |
296 | ### `useDroppable`
297 |
298 | The `useDroppable` hook makes any React element a potential drop target. It returns:
299 |
300 | - `droppable`: A function that wraps your component, making it a drop target
301 | - `hovered`: Data about the draggable currently hovering over this element (or `null` if none)
302 |
303 | Basic usage:
304 |
305 | ```jsx
306 | const DropZone = () => {
307 | const { droppable, hovered } = useDroppable({
308 | accepts: "ITEM", // Required: which draggable kinds to accept
309 | data: { zone: "main" }, // Optional: data to share with draggables
310 | onDrop({ data }) {
311 | // Optional: handle successful drops
312 | console.log("Dropped item:", data.id);
313 | },
314 | });
315 |
316 | // Change appearance when being hovered
317 | const isHovered = Boolean(hovered);
318 |
319 | return droppable(Drop here
);
320 | };
321 | ```
322 |
323 | ### `Overlay`
324 |
325 | The `Overlay` component renders the currently dragged element. It should be included once in your application:
326 |
327 | ```tsx
328 | import { Overlay } from "snapdrag";
329 |
330 | function App() {
331 | return (
332 |
333 | {/* Your app content */}
334 |
335 |
336 | {/* Required: Shows the dragged element */}
337 |
338 |
339 | );
340 | }
341 | ```
342 |
343 | You can add your own classes and styles to the overlay to make it fit your application.
344 |
345 | ## Draggable Lifecycle
346 |
347 | The draggable component goes through a lifecycle during drag interactions, with callbacks at each stage.
348 |
349 | ### `onDragStart`
350 |
351 | Called when the drag operation begins (after the user clicks and begins moving, and after `shouldDrag` if provided, returns `true`):
352 |
353 | ```jsx
354 | const { draggable } = useDraggable({
355 | kind: "CARD",
356 | onDragStart({ data, event, dragStartEvent, element }) {
357 | console.log("Started dragging card:", data.id);
358 | // Setup any state needed during dragging
359 | },
360 | });
361 | ```
362 |
363 | The callback receives an object with the following properties:
364 |
365 | - `data`: The draggable's data (from the `data` config option of `useDraggable`).
366 | - `event`: The `PointerEvent` that triggered the drag start (usually the first `pointermove` after `pointerdown` and `shouldDrag` validation).
367 | - `dragStartEvent`: The initial `PointerEvent` from `pointerdown` that initiated the drag attempt.
368 | - `element`: The DOM element that is being dragged (this is the element rendered in the `Overlay`).
369 |
370 | ### `onDragMove`
371 |
372 | Called on every pointer movement during dragging:
373 |
374 | ```jsx
375 | const { draggable } = useDraggable({
376 | kind: "CARD",
377 | onDragMove({ dropTargets, top, left, data, event, dragStartEvent, element }) {
378 | // dropTargets contains info about all drop targets under the pointer
379 | if (dropTargets.length > 0) {
380 | console.log("Over drop zone:", dropTargets[0].data.zone);
381 | }
382 |
383 | // top and left are the screen coordinates of the draggable
384 | console.log(`Position: ${left}px, ${top}px`);
385 | },
386 | });
387 | ```
388 |
389 | In addition to the properties from `onDragStart` (`data`, `dragStartEvent`, `element`), this callback receives:
390 |
391 | - `event`: The current `PointerEvent` from the `pointermove` handler.
392 | - `dropTargets`: An array of objects, each representing a droppable target currently under the pointer. Each object contains:
393 | - `data`: The `data` associated with the droppable (from its `useDroppable` configuration).
394 | - `element`: The DOM element of the droppable.
395 | - `top`: The calculated top screen coordinate of the draggable element in the overlay.
396 | - `left`: The calculated left screen coordinate of the draggable element in the overlay.
397 |
398 | **Note**: This callback is called frequently, so avoid expensive operations here.
399 |
400 | ### `onDragEnd`
401 |
402 | Called when the drag operation completes (on `pointerup`):
403 |
404 | ```jsx
405 | const { draggable } = useDraggable({
406 | kind: "CARD",
407 | onDragEnd({ dropTargets, top, left, data, event, dragStartEvent, element }) {
408 | if (dropTargets.length > 0) {
409 | console.log("Dropped on:", dropTargets[0].data.zone);
410 | } else {
411 | console.log("Dropped outside of any drop zone");
412 | // Handle "cancel" logic
413 | }
414 | },
415 | });
416 | ```
417 |
418 | Receives the same properties as `onDragMove` (`data`, `event`, `dragStartEvent`, `element`, `dropTargets`, `top`, `left`).
419 | If the user dropped the element on valid drop targets, `dropTargets` will contain them; otherwise, it will be an empty array.
420 | The `top` and `left` coordinates represent the final position of the draggable in the overlay just before it's hidden.
421 |
422 | ## Droppable Lifecycle
423 |
424 | 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.
425 |
426 | ### `onDragIn`
427 |
428 | Called when a draggable first enters this drop target:
429 |
430 | ```jsx
431 | const { droppable } = useDroppable({
432 | accepts: "CARD",
433 | onDragIn({ kind, data, event, element, dropElement, dropTargets }) {
434 | console.log(`${kind} entered drop zone`);
435 | // Change appearance, update state, etc.
436 | },
437 | });
438 | ```
439 |
440 | The callback receives an object with:
441 |
442 | - `kind`: The `kind` of the draggable that entered.
443 | - `data`: The `data` from the draggable.
444 | - `event`: The current `PointerEvent` from the `pointermove` handler.
445 | - `element`: The DOM element of the draggable.
446 | - `dropElement`: The DOM element of this droppable.
447 | - `dropTargets`: Array of all active drop targets under the pointer, including the current one. Each entry contains:
448 | - `data`: The `data` from the droppable (from its `useDroppable` configuration).
449 | - `element`: The DOM element of the droppable.
450 |
451 | This is called once when a draggable enters and can be used to trigger animations or state changes.
452 |
453 | ### `onDragMove` (Droppable)
454 |
455 | Called as a draggable moves _within_ the drop target:
456 |
457 | ```jsx
458 | const { droppable } = useDroppable({
459 | accepts: "CARD",
460 | onDragMove({ kind, data, event, element, dropElement, dropTargets }) {
461 | // Calculate position within the drop zone
462 | const rect = dropElement.getBoundingClientRect();
463 | const x = event.clientX - rect.left;
464 | const y = event.clientY - rect.top;
465 |
466 | console.log(`Position in drop zone: ${x}px, ${y}px`);
467 | },
468 | });
469 | ```
470 |
471 | 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.
472 |
473 | ### `onDragOut`
474 |
475 | Called when a draggable leaves the drop target:
476 |
477 | ```jsx
478 | const { droppable } = useDroppable({
479 | accepts: "CARD",
480 | onDragOut({ kind, data, event, element, dropElement, dropTargets }) {
481 | console.log(`${kind} left drop zone`);
482 | // Revert animations, update state, etc.
483 | },
484 | });
485 | ```
486 |
487 | 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.
488 |
489 | ### `onDrop`
490 |
491 | Called when a draggable is successfully dropped on this target:
492 |
493 | ```jsx
494 | const { droppable } = useDroppable({
495 | accepts: "CARD",
496 | onDrop({ kind, data, event, element, dropElement, dropTargets }) {
497 | console.log(`${kind} was dropped with data:`, data);
498 | // Handle the dropped item
499 | },
500 | });
501 | ```
502 |
503 | 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.
504 |
505 | ## Common Patterns
506 |
507 | ### Two-way Data Exchange
508 |
509 | Snapdrag makes it simple for draggables and droppables to talk to each other by exchanging data in both directions:
510 |
511 | ```jsx
512 | // Draggable component accessing droppable data
513 | const { draggable } = useDraggable({
514 | kind: "CARD",
515 | data: { id: "card-1", color: "red" },
516 | onDragMove({ dropTargets }) {
517 | if (dropTargets.length > 0) {
518 | // Read data from the drop zone underneath
519 | const dropZoneType = dropTargets[0].data.type;
520 | console.log(`Over ${dropZoneType} zone`);
521 | }
522 | },
523 | });
524 |
525 | // Droppable component accessing draggable data
526 | const { droppable, hovered } = useDroppable({
527 | accepts: "CARD",
528 | data: { type: "inbox" },
529 | onDragIn({ data }) {
530 | console.log(`Card ${data.id} entered inbox`);
531 | },
532 | });
533 | ```
534 |
535 | This pattern is especially useful for adapting the UI based on the interaction context.
536 |
537 | ### Dynamic Colors Example
538 |
539 | Here's how to create a draggable that changes color based on the droppable it's over:
540 |
541 | ```jsx
542 | // In DraggableSquare.tsx
543 | import { useState } from "react";
544 | import { useDraggable } from "snapdrag";
545 |
546 | export const DraggableSquare = ({ color: initialColor }) => {
547 | const [color, setColor] = useState(initialColor);
548 |
549 | const { draggable, isDragging } = useDraggable({
550 | kind: "SQUARE",
551 | data: { color },
552 | move: true,
553 | onDragMove({ dropTargets }) {
554 | if (dropTargets.length) {
555 | setColor(dropTargets[0].data.color);
556 | } else {
557 | setColor(initialColor);
558 | }
559 | },
560 | onDragEnd() {
561 | setColor(initialColor); // Reset on drop
562 | },
563 | });
564 |
565 | return draggable(
566 |
573 | {isDragging ? "Dragging" : "Drag me"}
574 |
575 | );
576 | };
577 |
578 | // In DroppableSquare.tsx
579 | import { useDroppable } from "snapdrag";
580 |
581 | export const DroppableSquare = ({ color }) => {
582 | const [text, setText] = useState("Drop here");
583 |
584 | const { droppable } = useDroppable({
585 | accepts: "SQUARE",
586 | data: { color }, // Share this color with draggables
587 | onDrop({ data }) {
588 | setText(`Dropped ${data.color}`);
589 | },
590 | });
591 |
592 | return droppable(
593 |
594 | {text}
595 |
596 | );
597 | };
598 | ```
599 |
600 | ### Dynamic Border Example
601 |
602 | This example shows how to create a visual indication of where an item will be dropped:
603 |
604 | ```jsx
605 | import { useState } from "react";
606 | import { useDroppable } from "snapdrag";
607 |
608 | export const DroppableSquare = ({ color }) => {
609 | const [text, setText] = useState("Drop here");
610 | const [borderPosition, setBorderPosition] = useState("");
611 |
612 | const { droppable } = useDroppable({
613 | accepts: "SQUARE",
614 | onDragMove({ event, dropElement }) {
615 | // Calculate which quadrant of the square the pointer is in
616 | const { top, left, height } = dropElement.getBoundingClientRect();
617 | const x = event.clientX - left;
618 | const y = event.clientY - top;
619 |
620 | // Set border on the appropriate side
621 | if (x / y < 1.0) {
622 | if (x / (height - y) < 1.0) {
623 | setBorderPosition("borderLeft");
624 | } else {
625 | setBorderPosition("borderBottom");
626 | }
627 | } else {
628 | if (x / (height - y) < 1.0) {
629 | setBorderPosition("borderTop");
630 | } else {
631 | setBorderPosition("borderRight");
632 | }
633 | }
634 | },
635 | onDragOut() {
636 | setBorderPosition(""); // Remove border when draggable leaves
637 | },
638 | onDrop({ data }) {
639 | setText(`Dropped ${data.color}`);
640 | setBorderPosition(""); // Remove border after drop
641 | },
642 | });
643 |
644 | // Add border to appropriate side
645 | const style = {
646 | backgroundColor: color,
647 | [borderPosition]: "10px solid red",
648 | };
649 |
650 | return droppable(
651 |
652 | {text}
653 |
654 | );
655 | };
656 | ```
657 |
658 | ### Multiple Drop Targets
659 |
660 | Snapdrag handles the case where multiple drop targets overlap:
661 |
662 | ```jsx
663 | const { draggable } = useDraggable({
664 | kind: "ITEM",
665 | onDragMove({ dropTargets }) {
666 | // Sort by order to find the topmost
667 | const sorted = [...dropTargets].sort((a, b) => b.data.order - a.data.order);
668 |
669 | if (sorted.length) {
670 | console.log(`Topmost target: ${sorted[0].data.name}`);
671 | }
672 | },
673 | });
674 |
675 | // ... somewere in your code
676 |
677 | const { droppable } = useDroppable({
678 | accepts: "ITEM",
679 | data: { order: 10 },
680 | });
681 | ```
682 |
683 | You also can you DOM elements to get the topmost drop target:
684 |
685 | ```tsx
686 | const { draggable } = useDraggable({
687 | kind: "ITEM",
688 | onDragMove({ dropTargets }) {
689 | // Sort by order to find the topmost
690 |
691 | const sorted = [...dropTargets].sort((a, b) => {
692 | // access drop target element instead of data
693 | const aIndex = a.element.getComputedStyle().zIndex || 0;
694 | const bIndex = b.element.getComputedStyle().zIndex || 0;
695 |
696 | return bIndex - aIndex;
697 | });
698 |
699 | if (sorted.length) {
700 | console.log(`Topmost target: ${sorted[0].data.name}`);
701 | }
702 | },
703 | });
704 | ```
705 |
706 | ### Drag Threshold
707 |
708 | For finer control, you can start dragging only after the pointer has moved a certain distance:
709 |
710 | ```jsx
711 | const { draggable } = useDraggable({
712 | kind: "ITEM",
713 | shouldDrag({ event, dragStartEvent }) {
714 | // Calculate distance from start position
715 | const dx = event.clientX - dragStartEvent.clientX;
716 | const dy = event.clientY - dragStartEvent.clientY;
717 | const distance = Math.sqrt(dx * dx + dy * dy);
718 |
719 | // Only start dragging after moving 5px
720 | return distance > 5;
721 | },
722 | });
723 | ```
724 |
725 | ### Touch Support
726 |
727 | 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.
728 |
729 | 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.
730 |
731 | ## Examples
732 |
733 | Snapdrag includes several examples that demonstrate its capabilities, from simple to complex use cases.
734 |
735 | ### Basic: Colored Squares
736 |
737 | The simplest example shows dragging a colored square onto a drop target:
738 |
739 |
740 |
741 |
742 |
743 | This demonstrates the fundamentals of drag-and-drop with Snapdrag:
744 |
745 | - Defining a draggable with `kind` and `data`
746 | - Creating a drop target that `accepts` the draggable
747 | - Handling the `onDrop` event
748 |
749 | [Try it on CodeSandbox](https://codesandbox.io/p/sandbox/snapdrag-simple-squares-8rw96s)
750 |
751 | ### Intermediate: Simple List
752 |
753 | A sortable list where items can be reordered by dragging:
754 |
755 |
756 |
757 |
758 |
759 | This example demonstrates:
760 |
761 | - Using data to identify list items
762 | - Visual feedback during dragging (blue insertion line)
763 | - Reordering items in a state array on drop
764 |
765 | [Try it on CodeSandbox](https://codesandbox.io/p/sandbox/snapdrag-simple-list-w4njk5)
766 |
767 | ### Advanced: List with Animations
768 |
769 | A more sophisticated list with smooth animations:
770 |
771 |
772 |
773 |
774 |
775 | This example showcases:
776 |
777 | - CSS transitions for smooth animations
778 | - A special drop area for appending items to the end
779 | - Animated placeholders that create space for dropped items
780 |
781 | [Try it on CodeSandbox](https://codesandbox.io/p/sandbox/snapdrag-advanced-list-5p44wd)
782 |
783 | ### Expert: Kanban Board
784 |
785 | A full kanban board with multiple columns and draggable cards:
786 |
787 |
788 |
789 |
790 |
791 | This complex example demonstrates advanced features:
792 |
793 | - Multiple drop targets with different behaviors
794 | - Conditional acceptance of draggables
795 | - Smooth animations during drag operations
796 | - Two-way data exchange between components
797 | - Touch support with drag threshold
798 | - Item addition and removal
799 |
800 | All this is achieved in just about 200 lines of code (excluding state management and styling).
801 |
802 | [Try it on CodeSandbox](https://codesandbox.io/p/sandbox/snapdrag-kanban-board-jlj4wc)
803 |
804 | ## API Reference
805 |
806 | ### `useDraggable` Configuration
807 |
808 | The `useDraggable` hook accepts a configuration object with these options:
809 |
810 | | Option | Type | Description |
811 | | ------------- | ---------------------- | ------------------------------------------------------------------------------------- |
812 | | `kind` | `string` or `symbol` | **Required.** Identifies this draggable type |
813 | | `data` | `object` or `function` | Data to share with droppables. Can be a static object or a function that returns data |
814 | | `disabled` | `boolean` | When `true`, disables dragging functionality |
815 | | `move` | `boolean` | When `true`, moves the component instead of cloning it to the overlay |
816 | | `component` | `function` | Provides a custom component to show while dragging |
817 | | `placeholder` | `function` | Custom component to show in place of the dragged item |
818 | | `offset` | `object` or `function` | Controls positioning relative to cursor |
819 |
820 | **Event Callbacks:**
821 |
822 | | Callback | Description |
823 | | ------------- | ---------------------------------------------------------------------------- |
824 | | `shouldDrag` | Function determining if dragging should start. Must return `true` or `false` |
825 | | `onDragStart` | Called when drag begins |
826 | | `onDragMove` | Called on every pointer move while dragging |
827 | | `onDragEnd` | Called when dragging ends |
828 |
829 | #### Detailed Configuration Description
830 |
831 | ##### `kind` (Required)
832 |
833 | Defines the type of the draggable. It must be a unique string or symbol.
834 |
835 | ```jsx
836 | const { draggable } = useDraggable({
837 | kind: "SQUARE", // Identify this as a "SQUARE" type
838 | });
839 | ```
840 |
841 | ##### `data`
842 |
843 | Data associated with the draggable. It can be a static object or a function that returns an object:
844 |
845 | ```jsx
846 | // Static object
847 | const { draggable } = useDraggable({
848 | kind: "SQUARE",
849 | data: { color: "red", id: "square-1" },
850 | });
851 |
852 | // Function (calculated at drag start)
853 | const { draggable } = useDraggable({
854 | kind: "SQUARE",
855 | data: ({ dragElement, dragStartEvent }) => ({
856 | id: dragElement.id,
857 | position: { x: dragStartEvent.clientX, y: dragStartEvent.clientY },
858 | }),
859 | });
860 | ```
861 |
862 | ##### `disabled`
863 |
864 | When `true`, temporarily disables dragging:
865 |
866 | ```jsx
867 | const { draggable } = useDraggable({
868 | kind: "SQUARE",
869 | disabled: !canDrag, // Disable based on some condition
870 | });
871 | ```
872 |
873 | ##### `move`
874 |
875 | When `true`, the original component is moved during dragging instead of creating a clone:
876 |
877 | ```jsx
878 | const { draggable } = useDraggable({
879 | kind: "SQUARE",
880 | move: true, // Move the actual component
881 | });
882 | ```
883 |
884 | 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.
885 |
886 | ##### `component`
887 |
888 | A function that returns a custom component to be shown during dragging:
889 |
890 | ```jsx
891 | const { draggable } = useDraggable({
892 | kind: "SQUARE",
893 | component: ({ data, props }) => ,
894 | });
895 | ```
896 |
897 | ##### `placeholder`
898 |
899 | A function that returns a component to be shown in place of the dragged item:
900 |
901 | ```jsx
902 | const { draggable } = useDraggable({
903 | kind: "SQUARE",
904 | placeholder: ({ data, props }) => ,
905 | });
906 | ```
907 |
908 | When specified, the `move` option is ignored.
909 |
910 | ##### `offset`
911 |
912 | Controls the offset of the dragging component relative to the cursor:
913 |
914 | ```jsx
915 | // Static offset
916 | const { draggable } = useDraggable({
917 | kind: "SQUARE",
918 | offset: { top: 10, left: 10 }, // 10px down and right from cursor
919 | });
920 |
921 | // Dynamic offset
922 | const { draggable } = useDraggable({
923 | kind: "SQUARE",
924 | offset: ({ element, event, data }) => {
925 | // Calculate based on event or element position
926 | return { top: 0, left: 0 };
927 | },
928 | });
929 | ```
930 |
931 | If not specified, the offset is calculated to maintain the element's initial position relative to the cursor.
932 |
933 | #### Callback Details
934 |
935 | ##### `shouldDrag`
936 |
937 | Function that determines if dragging should start. It's called on every pointer move until it returns `true` or the drag attempt ends:
938 |
939 | ```jsx
940 | const { draggable } = useDraggable({
941 | kind: "SQUARE",
942 | shouldDrag: ({ event, dragStartEvent, element, data }) => {
943 | // Only drag if shifted 10px horizontally
944 | return Math.abs(event.clientX - dragStartEvent.clientX) > 10;
945 | },
946 | });
947 | ```
948 |
949 | ##### `onDragStart`
950 |
951 | Called when dragging begins (after `shouldDrag` returns `true`):
952 |
953 | ```jsx
954 | const { draggable } = useDraggable({
955 | kind: "SQUARE",
956 | onDragStart: ({ event, dragStartEvent, element, data }) => {
957 | console.log("Drag started at:", event.clientX, event.clientY);
958 | // Setup any initial state needed during drag
959 | },
960 | });
961 | ```
962 |
963 | ##### `onDragMove`
964 |
965 | Called on every pointer move during dragging:
966 |
967 | ```jsx
968 | const { draggable } = useDraggable({
969 | kind: "SQUARE",
970 | onDragMove: ({ event, dragStartEvent, element, data, dropTargets, top, left }) => {
971 | // Current drop targets under the pointer
972 | if (dropTargets.length) {
973 | console.log("Over drop zone:", dropTargets[0].data.name);
974 | }
975 |
976 | // Current position of the draggable
977 | console.log("Position:", top, left);
978 | },
979 | });
980 | ```
981 |
982 | 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).
983 |
984 | ##### `onDragEnd`
985 |
986 | Called when dragging ends:
987 |
988 | ```jsx
989 | const { draggable } = useDraggable({
990 | kind: "SQUARE",
991 | onDragEnd: ({ event, dragStartEvent, element, data, dropTargets }) => {
992 | if (dropTargets.length) {
993 | console.log("Dropped on:", dropTargets[0].data.name);
994 | } else {
995 | console.log("Dropped outside any drop target");
996 | // Handle "cancel" case
997 | }
998 | },
999 | });
1000 | ```
1001 |
1002 | ### `useDroppable` Configuration
1003 |
1004 | The `useDroppable` hook accepts a configuration object with these options:
1005 |
1006 | | Option | Type | Description |
1007 | | ---------- | ------------------------------------------ | -------------------------------------------- |
1008 | | `accepts` | `string`, `symbol`, `array`, or `function` | **Required.** What draggable kinds to accept |
1009 | | `data` | `object` | Data to share with draggables |
1010 | | `disabled` | `boolean` | When `true`, disables dropping |
1011 |
1012 | **Event Callbacks:**
1013 |
1014 | | Callback | Description |
1015 | | ------------ | ---------------------------------------------------- |
1016 | | `onDragIn` | Called when a draggable enters this droppable |
1017 | | `onDragOut` | Called when a draggable leaves this droppable |
1018 | | `onDragMove` | Called when a draggable moves within this droppable |
1019 | | `onDrop` | Called when a draggable is dropped on this droppable |
1020 |
1021 | #### Detailed Configuration Description
1022 |
1023 | ##### `accepts` (Required)
1024 |
1025 | Defines what kinds of draggables this drop target can accept:
1026 |
1027 | ```jsx
1028 | // Accept a single kind
1029 | const { droppable } = useDroppable({
1030 | accepts: "SQUARE",
1031 | });
1032 |
1033 | // Accept multiple kinds
1034 | const { droppable } = useDroppable({
1035 | accepts: ["SQUARE", "CIRCLE"],
1036 | });
1037 |
1038 | // Use a function for more complex logic
1039 | const { droppable } = useDroppable({
1040 | accepts: ({ kind, data }) => {
1041 | // Check both kind and data to determine acceptance
1042 | return kind === "SQUARE" && data.color === "red";
1043 | },
1044 | });
1045 | ```
1046 |
1047 | ##### `data`
1048 |
1049 | Data associated with the droppable area:
1050 |
1051 | ```jsx
1052 | const { droppable } = useDroppable({
1053 | accepts: "SQUARE",
1054 | data: {
1055 | zoneId: "dropzone-1",
1056 | capacity: 5,
1057 | color: "blue",
1058 | },
1059 | });
1060 | ```
1061 |
1062 | This data is accessible to draggables through the `dropTargets` array in their callbacks.
1063 |
1064 | ##### `disabled`
1065 |
1066 | When `true`, temporarily disables dropping:
1067 |
1068 | ```jsx
1069 | const { droppable } = useDroppable({
1070 | accepts: "SQUARE",
1071 | disabled: isFull, // Disable based on some condition
1072 | });
1073 | ```
1074 |
1075 | #### Callback Details
1076 |
1077 | ##### `onDragIn`
1078 |
1079 | Called when a draggable of an accepted kind first enters this drop target:
1080 |
1081 | ```jsx
1082 | const { droppable } = useDroppable({
1083 | accepts: "SQUARE",
1084 | onDragIn: ({ kind, data, event, element, dropElement, dropTargets }) => {
1085 | console.log(`${kind} entered with data:`, data);
1086 | // Change appearance, play sound, etc.
1087 | },
1088 | });
1089 | ```
1090 |
1091 | Arguments:
1092 |
1093 | - `kind` - The kind of the draggable
1094 | - `data` - The data from the draggable
1095 | - `event` - The current pointer event
1096 | - `element` - The draggable element
1097 | - `dropElement` - The droppable element
1098 | - `dropTargets` - Array of all current drop targets under the pointer
1099 |
1100 | ##### `onDragOut`
1101 |
1102 | Called when a draggable leaves this drop target:
1103 |
1104 | ```jsx
1105 | const { droppable } = useDroppable({
1106 | accepts: "SQUARE",
1107 | onDragOut: ({ kind, data, event, element, dropElement, dropTargets }) => {
1108 | console.log(`${kind} left the drop zone`);
1109 | // Revert appearance changes, etc.
1110 | },
1111 | });
1112 | ```
1113 |
1114 | Arguments are the same as `onDragIn`.
1115 |
1116 | ##### `onDragMove`
1117 |
1118 | Called when a draggable moves within this drop target:
1119 |
1120 | ```jsx
1121 | const { droppable } = useDroppable({
1122 | accepts: "SQUARE",
1123 | onDragMove: ({ kind, data, event, element, dropElement, dropTargets }) => {
1124 | // Calculate position within drop zone
1125 | const rect = dropElement.getBoundingClientRect();
1126 | const relativeX = event.clientX - rect.left;
1127 | const relativeY = event.clientY - rect.top;
1128 |
1129 | console.log(`Position in zone: ${relativeX}px, ${relativeY}px`);
1130 | },
1131 | });
1132 | ```
1133 |
1134 | Arguments are the same as `onDragIn`.
1135 |
1136 | ##### `onDrop`
1137 |
1138 | Called when a draggable is dropped on this target:
1139 |
1140 | ```jsx
1141 | const { droppable } = useDroppable({
1142 | accepts: "SQUARE",
1143 | onDrop: ({ kind, data, event, element, dropElement, dropTargets }) => {
1144 | console.log(`${kind} was dropped with data:`, data);
1145 | // Handle the dropped item (update state, etc.)
1146 | },
1147 | });
1148 | ```
1149 |
1150 | Arguments are the same as the other callbacks.
1151 |
1152 | ## Plugins
1153 |
1154 | 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.
1155 |
1156 | ### Scroller Plugin
1157 |
1158 | 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.
1159 |
1160 | **Initialization**
1161 |
1162 | To use the scroller plugin, first create an instance of it by calling `createScroller(config)`.
1163 |
1164 | ```typescript
1165 | import { createScroller } from "snapdrag/plugins";
1166 |
1167 | const scroller = createScroller({
1168 | x: true, // Enable horizontal scrolling with default settings
1169 | y: { threshold: 150, speed: 1000, distancePower: 2 }, // Enable vertical scrolling with custom settings
1170 | });
1171 | ```
1172 |
1173 | **Configuration Options (`ScrollerConfig`)**
1174 |
1175 | - `x`: (Optional) Enables or configures horizontal scrolling.
1176 | - `boolean`: If `true`, uses default settings. If `false` or omitted, horizontal scrolling is disabled.
1177 | - `object (AxisConfig)`: Allows fine-tuning of horizontal scrolling behavior:
1178 | - `threshold` (number, default: `100`): The distance in pixels from the container's edge at which scrolling should begin.
1179 | - `speed` (number, default: `2000`): The maximum scroll speed in pixels per second when the pointer is at the very edge of the container.
1180 | - `distancePower` (number, default: `1.5`): Controls the acceleration of scrolling as the pointer gets closer to the edge. A higher value means faster acceleration.
1181 | - `y`: (Optional) Enables or configures vertical scrolling. Accepts the same `boolean` or `object (AxisConfig)` values as `x`.
1182 |
1183 | **Usage with `useDraggable`**
1184 |
1185 | 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.
1186 |
1187 | ```jsx
1188 | import { useDraggable } from "snapdrag";
1189 | import { createScroller } from "snapdrag/plugins";
1190 | import { useRef, useEffect, useState } from "react";
1191 |
1192 | // Initialize the scroller plugin
1193 | const scrollerPlugin = createScroller({ x: true, y: true });
1194 |
1195 | const DraggableComponent = () => {
1196 | // State to hold the container element once it's mounted
1197 | const [scrollContainer, setScrollContainer] = useState(null);
1198 |
1199 | const { draggable } = useDraggable({
1200 | kind: "ITEM",
1201 | data: { id: "my-item" },
1202 | plugins: [scrollerPlugin(scrollContainer)],
1203 | });
1204 |
1205 | return (
1206 |
1210 |
1211 | {/* Inner content larger than container */}
1212 | {draggable(
1213 |
1214 | Drag me
1215 |
1216 | )}
1217 | {/* More draggable items or content here */}
1218 |
1219 |
1220 | );
1221 | };
1222 | ```
1223 |
1224 | **How it Works**
1225 |
1226 | 1. **Initialization**: `createScroller` returns a new scroller function configured with your desired settings.
1227 | 2. **Plugin Attachment**: When you pass `scrollerPlugin(containerElement)` to `useDraggable`, Snapdrag calls the appropriate lifecycle methods of the plugin (`onDragStart`, `onDragMove`, `onDragEnd`).
1228 | 3. **Drag Monitoring**: During a drag operation, `onDragMove` is continuously called. The scroller plugin checks the pointer's position relative to the specified `containerElement`.
1229 | 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.
1230 | 5. **Scrolling Speed**: The scrolling speed increases polynomially (based on `distancePower`) as the pointer gets closer to the edge, up to the maximum `speed`.
1231 | 6. **Animation Loop**: Scrolling is performed using `requestAnimationFrame` for smooth animation.
1232 | 7. **Cleanup**: When the drag ends (`onDragEnd`) or the component unmounts, the plugin cleans up any active animation frames.
1233 |
1234 | **Important Considerations:**
1235 |
1236 | - The `containerElement` passed to the scroller function must be the actual scrollable DOM element.
1237 | - Ensure the `containerElement` has `overflow: auto` or `overflow: scroll` CSS properties set for the respective axes you want to enable scrolling on.
1238 | - 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.
1239 | - 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.
1240 |
1241 | 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.
1242 |
1243 | ## Browser Compatibility
1244 |
1245 | Snapdrag is compatible with all modern browsers that support Pointer Events. This includes:
1246 |
1247 | - Chrome 55+
1248 | - Firefox 59+
1249 | - Safari 13.1+
1250 | - Edge 18+
1251 |
1252 | Mobile devices are also supported as long as they support Pointer Events.
1253 |
1254 | ## License
1255 |
1256 | MIT
1257 |
1258 | ## Author
1259 |
1260 | Eugene Daragan
1261 |
--------------------------------------------------------------------------------