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 — 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.
<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
npm install @emhamitay/ghostdrop2 · Add to your app
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
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
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
Components
DndProvider
RequiredWrap your app (or the DnD area) with this. Sets up required DOM styles. No props needed.
GhostLayer
OptionalRenders 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
ComponentMakes 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
ComponentDefines 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). |
SortableDropGroup
ComponentWraps 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. |
SortableDraggable
ComponentA 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
ComponentCombines 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
HookThe underlying primitive for drag behavior.
useDrag({ id, sortId?, type?, data? })
{ onPointerDown }
useDrop
HookThe underlying primitive for drop zone detection.
useDrop({ id, onDrop?, onHoverEnter?, onHoverLeave? })
{ dropRef, isHover }
useSortable
HookTracks hover state and position for a sortable item.
useSortable({ id, direction? })
{ ref, isHover, isActive }
useSortableDrop
HookRegisters a sortable group and handles sort completion.
useSortableDrop({ items, onSorted, indexKey?, mode? })
sortId (string)