React drag-and-drop library · MIT · Zero dependencies

Ghost Drop

The drag preview renders via a React portal — above every z-index, never clipped by overflow: hidden. Clean API, full control over what you drag and where it lands.

Built from scratch because existing libraries don't solve this well.

Why this framework?

Built from scratch to solve real problems — not a wrapper around another DnD library.

🎯

Dynamic drag with any callback

Every drop zone runs your own callback. Log, toast, mutate state, call an API — the framework doesn't dictate what happens on drop.

↕️

Sorting — with or without drag-out

Add sortable reordering to any list. Items can be sorted within the group, or dragged out to a separate zone entirely.

🗂️

Multiple independent groups

Define as many groups as you need, each with its own items, sort logic, and callback. They stay completely isolated.

👻

Bring your own ghost

Use the built-in GhostLayer for instant visual feedback, or skip it and build your own custom drag preview. Fully optional.

🧱

No DOM layering issues

Built specifically to avoid z-index, portal, and stacking-context bugs that plague other DnD libraries. The ghost renders at body level via a React portal.

How it works

Three concepts. Once you get these, everything else follows naturally.

The flow

user drags

<Draggable id="x">

framework tracks it

ghost follows cursor

user releases over

<Droppable id="zone">

your callback fires

onDrop({ id: 'x' })

<Draggable>

A wrapper around whatever you want to drag — a card, a row, an image, anything. The only thing you must give it is an id. That id is what comes back to you when it lands somewhere, so you know which item was moved.

The framework doesn't know what your data is. It just hands your id back to you and lets you decide what to do.

📥

<Droppable>

Marks an area on the page as a valid landing zone. When the user releases a draggable over it, onDrop(item) fires — and item is:

{ id: 'your-id', type: 'default', data: anything }

id — what was dropped. type — optional label you set on Draggable to filter drops. data — any extra info you attached.

👻

<GhostLayer>

Completely optional. Renders a visual copy of the dragged element that follows the cursor while dragging.

It uses a React portal to render at the top of the page — so it never gets clipped by overflow: hidden, never loses to z-index wars. That was actually the main reason this framework was built from scratch.

Don't like it? Skip it. Build your own. The drag-and-drop still works without it.

↕️

<SortableDropGroup> + <SortableDraggable>

A group of items that can reorder themselves when you drag one over another. Think reorderable lists, kanban columns, priority queues.

You give it your items array (your current order) and an onSorted callback. When the user finishes dragging, onSorted gives you back the same array in the new order — you just set your state.

onSorted={(newItems) => setItems(newItems)}
What about <DroppableSortableWrapper>? It's just Droppable + SortableDropGroup combined into one component — for when you want a zone that accepts drops from outside and lets items inside reorder. Instead of composing two components, you use one. Same props, same concepts.

Quick Start

Three steps. That's it.

1 · Install

Terminal
npm install @emhamitay/ghostdrop

2 · Add to your app

App.tsx
import { useState } from 'react';
import type { DndItem } from '@emhamitay/ghostdrop';
import { DndProvider, GhostLayer, Draggable, Droppable } from '@emhamitay/ghostdrop';

function App() {
  return (
    <DndProvider>
      <GhostLayer />

      <Draggable id="card-1">
        <div>Drag me</div>
      </Draggable>

      <Droppable id="zone-1" onDrop={(item: DndItem) => console.log('dropped:', item.id)}>
        <div>Drop here</div>
      </Droppable>
    </DndProvider>
  );
}

3 · Try it — live

✋ Drag me
Drop zone

Want more? See the Examples below for sorting, groups, and custom callbacks.

Examples

From a single drop to complex multi-group interactions — each example is live and interactive.

Files

📄 report.pdf
🖼️ photo.png
📝 notes.txt
📥Drop here
Example1.tsx
import { useState } from 'react';
import type { DndItem } from '@emhamitay/ghostdrop';
import { DndProvider, GhostLayer, Draggable, Droppable } from '@emhamitay/ghostdrop';

function App() {
  const [dropped, setDropped] = useState<string | null>(null);

  return (
    <DndProvider>
      <GhostLayer />

      <Draggable id="file-1">
        <div>📄 report.pdf</div>
      </Draggable>

      <Droppable
        id="inbox"
        onDrop={(item: DndItem) => setDropped(item.id)}
      >
        <div className="drop-zone">
          {dropped ? `${dropped} landed!` : '📥 Drop here'}
        </div>
      </Droppable>
    </DndProvider>
  );
}

API Reference

All components, props, hooks, and enums.

Components

DndProvider

Required

Wrap your app (or the DnD area) with this. Sets up required DOM styles. No props needed.

Note: Place GhostLayer directly inside DndProvider, ideally at the root level.

GhostLayer

Optional

Renders a clone of the dragged element that follows the cursor. Uses a React portal to render at body level, avoiding z-index and overflow issues. Completely optional — build your own if needed.

Draggable

Component

Makes any element draggable. Clones the child element and attaches drag behavior.

Prop Type Description
id string Unique ID for this draggable item. Passed to onDrop callbacks.
type string? Optional type identifier (default: "default"). Useful for filtering in drop callbacks.
data any? Optional metadata attached to the drag item. Available in onDrop as item.data.
children ReactElement The element to make draggable. Must be a single React element.
className string? Extra classes merged onto the child element.

Droppable

Component

Defines a drop zone. Calls onDrop when an item is released over it. Supports render props for access to isHover state.

Prop Type Description
id string Unique ID for this drop zone.
onDrop (item: DndItem) => void? Called when an item is dropped. item = { id, type, data }.
onHoverEnter (item: DndItem) => void? Called when a dragged item enters this zone. Use instead of (or alongside) isHover for side-effects like toasts or previews.
onHoverLeave (item: DndItem) => void? Called when a dragged item leaves this zone.
children node | (isHover, ref) => node Static children, or a render function receiving isHover (boolean) and ref (attach to your drop element).
className string? CSS classes for the wrapper div (only when children is static, not a render function).
Note: import type { DndItem } from '@emhamitay/ghostdrop'; — item: { id: string; type: string; data: Record<string, unknown> }

SortableDropGroup

Component

Wraps a list of sortable items. Handles reordering logic and provides sort context to children. Use alongside SortableDraggable.

Prop Type Description
items object[] Array of items to sort. Each must have a unique id property.
onSorted (newItems: T[]) => void Called with the reordered array after a sort completes.
direction SORT_DIRECTION? Layout direction for the whole group. Vertical (default), Horizontal, or Grid. Sets the shift axis for the animation.
layoutAnimation "shift" | "none"? Controls the sort animation. "shift" (default): items slide to make space as you drag. "none": classic instant reorder on drop, no movement animation.
indexKey string? Property name used for sort order. Default: "index". Change if your items use a different field.
mode SORT_MODE? Insert (default): shift items into position. Switch: swap dragged with hovered.
children ReactNode Should contain SortableDraggable components.
className string? CSS classes for the wrapper div.
Note: Dropping in empty space (outside all items) cancels the sort — items return to their original positions. Press Escape at any time to cancel a drag in progress.

SortableDraggable

Component

A draggable item that is aware of its sort group. Must be inside a SortableDropGroup (or DroppableSortableWrapper). Supports render props for full control.

Prop Type Description
id string Unique ID matching the item in the SortableDropGroup items array.
direction SORT_DIRECTION? Layout direction for sorting. Vertical (default), Horizontal, or Grid.
onHoverEnter (item: DndItem) => void? Called when a dragged item enters this sortable item.
onHoverLeave (item: DndItem) => void? Called when a dragged item leaves this sortable item.
children node | (renderProps) => node Static children get automatic grab cursor and shift animation. Render function receives { ref, isHover, isActive, onPointerDown, style } — spread style onto your element to get the shift transform.
className string? CSS classes (when children is static).

DroppableSortableWrapper

Component

Combines Droppable + SortableDropGroup into one component. Use when you want a zone that accepts drops from outside AND allows internal reordering.

Prop Type Description
id string? Drop zone ID (for external drops).
items object[] Items to sort.
onSorted (newItems) => void Called when internal sort completes.
onDrop (item: DndItem) => void? Called when an external item is dropped in.
onHoverEnter (item: DndItem) => void? Called when a dragged item enters this zone.
onHoverLeave (item: DndItem) => void? Called when a dragged item leaves this zone.
children node | ({ isHover }) => node Render function receives isHover. Use it to style the drop zone on hover.
indexKey string? Sort order key. Default: "index".
mode SORT_MODE? Insert or Switch.
direction SORT_DIRECTION? Layout direction for the group. Vertical (default), Horizontal, or Grid.
layoutAnimation LAYOUT_ANIMATION? Shift (default): items slide to make space. None: instant reorder on drop.
className string? CSS classes for the outer wrapper.

TypeScript Types

// Import types alongside components

import type { DndItem } from '@emhamitay/ghostdrop';


// The shape passed to onDrop, onHoverEnter, onHoverLeave

type DndItem = {

id: string;

type: string;

data: Record<string, unknown>;

}

All callback props (onDrop, onHoverEnter, onHoverLeave) receive a DndItem. Cast item.data to your own type for full type safety.

Enums

SORT_MODE

Enum
Value Description
SORT_MODE.Insert Default. Inserts the dragged item at the new position, shifting others.
SORT_MODE.Switch Swaps the dragged item directly with the hovered item.

SORT_DIRECTION

Enum
Value Description
SORT_DIRECTION.Vertical Default. Detects up/down movement for vertical lists.
SORT_DIRECTION.Horizontal Detects left/right movement for horizontal lists.
SORT_DIRECTION.Grid Detects both axes for grid layouts.

LAYOUT_ANIMATION

Enum
Value Description
LAYOUT_ANIMATION.Shift Default. Items slide to make space as you drag, showing exactly where the item will land.
LAYOUT_ANIMATION.None No animation. Items reorder instantly on drop. Use when you want full control over visual feedback.

Hooks (low-level)

These are the primitives the components are built on. Use them only when you need custom behavior that the components don't support.

useDrag

Hook

The underlying primitive for drag behavior.

Signature

useDrag({ id, sortId?, type?, data? })

Returns

{ onPointerDown }

Prefer using Draggable or SortableDraggable unless you need custom drag behavior.

useDrop

Hook

The underlying primitive for drop zone detection.

Signature

useDrop({ id, onDrop?, onHoverEnter?, onHoverLeave? })

Returns

{ dropRef, isHover }

Prefer using Droppable unless you need low-level access.

useSortable

Hook

Tracks hover state and position for a sortable item.

Signature

useSortable({ id, direction? })

Returns

{ ref, isHover, isActive }

useSortableDrop

Hook

Registers a sortable group and handles sort completion.

Signature

useSortableDrop({ items, onSorted, indexKey?, mode? })

Returns

sortId (string)