Drag & Drop
Powerful drag and drop components with scroll-aware positioning, auto-scroll, touch support, and fine-grained drop validation — zero third-party dependencies.
Setup
Drag & drop components work out of the box — no provider wrapping, no extra peer dependencies. Just import from fluxo-ui and go.
import { Draggable, Droppable } from 'fluxo-ui';
function MyList() {
return (
<>
<Draggable containerId="list" index={0} item={myItem}>
<div>Drag me</div>
</Draggable>
<Droppable containerId="target" index={0} onDrop={handleDrop}>
<div>Drop here</div>
</Droppable>
</>
);
}The built-in engine supports mouse, touch, pen, drag handles, delay activation, custom live previews, auto-scroll near container edges, and scroll-aware positioning — all with zero third-party dependencies. An optional DragDropProvider is still exported for backwards compatibility but is no longer required.
Basic Drag & Drop
Simple Drag & Drop
Draggable Items
Drop Zone
import { Draggable, Droppable } from 'fluxo-ui';
function DragDropExample() {
const [items] = useState(['Item 1', 'Item 2', 'Item 3']);
const [droppedItems, setDroppedItems] = useState([]);
const handleDrop = (source, target) => {
setDroppedItems([...droppedItems, source.item]);
};
return (
<div className="flex gap-8">
{/* Source */}
<div>
{items.map((item, index) => (
<Draggable
key={index}
containerId="source"
index={index}
item={item}
itemType="task"
>
<div className="bg-blue-600 px-4 py-3 rounded cursor-move">
{item}
</div>
</Draggable>
))}
</div>
{/* Drop Zone — built-in highlight indicator, no manual border logic needed */}
<Droppable
containerId="target"
index={0}
accept="task"
onDrop={handleDrop}
className="min-h-50 border-2 border-dashed border-gray-300 rounded-lg p-4"
>
{droppedItems.length > 0
? droppedItems.map((item, idx) => <div key={idx}>{item}</div>)
: 'Drop here'}
</Droppable>
</div>
);
}Multi-Container Drag & Drop
Kanban-style Board
To Do
Done
import { Draggable, Droppable } from 'fluxo-ui';
function KanbanBoard() {
const [items, setItems] = useState({
todo: ['Task A', 'Task B', 'Task C'],
done: [],
});
const handleDrop = (source, target) => {
const sourceContainer = source.containerId;
const targetContainer = target.containerId;
if (sourceContainer === targetContainer) return;
setItems(prev => ({
todo: sourceContainer === 'todo'
? prev.todo.filter((_, i) => i !== source.index)
: prev.todo,
done: targetContainer === 'done'
? [...prev.done, source.item]
: prev.done,
}));
};
return (
<div className="flex flex-col sm:flex-row gap-6 sm:gap-8">
<Droppable containerId="todo" index={0} accept="task" onDrop={handleDrop}>
{({ dropRef, isOver }) => (
<div ref={dropRef}>
{items.todo.map((item, index) => (
<Draggable
key={index}
containerId="todo"
index={index}
item={item}
itemType="task"
>
<div>{item}</div>
</Draggable>
))}
</div>
)}
</Droppable>
<Droppable containerId="done" index={0} accept="task" onDrop={handleDrop}>
{({ dropRef }) => (
<div ref={dropRef}>
{items.done.map((item, idx) => (
<div key={idx}>{item}</div>
))}
</div>
)}
</Droppable>
</div>
);
}Custom Styling with Render Props
Visual Feedback with Render Props
Available Features
Selected Features
<Draggable containerId="source" index={0} item={item} itemType="feature">
{({ isDragging, dragRef }) => (
<div
ref={dragRef}
className={`${isDragging ? 'opacity-50 scale-105' : ''}`}
>
{item}
</div>
)}
</Draggable>
<Droppable containerId="target" index={0} accept="feature" onDrop={handleDrop}>
{({ dropRef, isOver, canDrop }) => (
<div
ref={dropRef}
className={`${isOver && canDrop ? 'bg-green-500/20' : ''}`}
>
{isOver ? 'Release to drop' : 'Drop here'}
</div>
)}
</Droppable>Type-Based Drag & Drop Restrictions
Accept Only Specific Item Types
Images
Documents
Image Folder (Images Only)
Document Folder (Documents Only)
Recycling Bin (Accepts All)
Try dragging files to different folders. The Image Folder only accepts images, Document Folder only accepts documents, but the Recycling Bin accepts both types. Try dragging a document to the Image Folder — it won't be accepted!
import { Draggable, Droppable } from 'fluxo-ui';
function TypeBasedDragDrop() {
const [items, setItems] = useState({
images: [
{ id: 1, name: 'photo1.jpg', type: 'image' },
{ id: 2, name: 'photo2.png', type: 'image' },
],
documents: [
{ id: 3, name: 'report.pdf', type: 'document' },
],
imageFolder: [],
recyclingBin: [],
});
return (
<div className="grid grid-cols-3 gap-4">
{/* Images Source */}
<div>
{items.images.map((item, index) => (
<Draggable
key={item.id}
containerId="images"
index={index}
item={item}
itemType="image"
onRemove={(source) => {
setItems(prev => ({
...prev,
images: prev.images.filter((_, i) => i !== source.index),
}));
}}
>
<div>{item.name}</div>
</Draggable>
))}
</div>
{/* Image Folder - Only accepts images */}
<Droppable
containerId="image-folder"
index={0}
accept="image"
onDrop={(source) => {
setItems(prev => ({
...prev,
imageFolder: [...prev.imageFolder, source.item],
}));
}}
>
{({ dropRef, isOver, canDrop }) => (
<div
ref={dropRef}
className={`${isOver && canDrop ? 'bg-blue-500/20' : ''}`}
>
{items.imageFolder.length > 0
? items.imageFolder.map(item => <div key={item.id}>{item.name}</div>)
: 'Drop images here'}
</div>
)}
</Droppable>
{/* Recycling Bin - Accepts both types */}
<Droppable
containerId="bin"
index={0}
accept={['image', 'document']}
onDrop={(source) => {
setItems(prev => ({
...prev,
recyclingBin: [...prev.recyclingBin, source.item],
}));
}}
>
{({ dropRef }) => (
<div ref={dropRef}>
{items.recyclingBin.map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
)}
</Droppable>
</div>
);
}Drop Position — Before / After
Drop Position Auto (before / after insertion)
Hover the top half of any row to see the insertion line on the top edge; hover the bottom half to see it on the bottom edge. Dropping lands exactly where the line is shown.
// dropPosition="auto" splits each target in half and returns
// the exact insert index via position ('before' | 'after').
<Droppable
containerId="list"
index={i}
dropPosition="auto"
dropIndicator="line"
edgeThreshold={12}
onDrop={(source, target) => {
// target.index already reflects before/after choice
setItems((prev) => reorder(prev, source.index, target.index));
}}
>
{item}
</Droppable>Strict Drag Handles
Strict Drag Handles (handle-only activation)
Try dragging the card body — nothing happens. Drag from the grip icon on the left to reorder. The body remains fully interactive (text selection, button clicks).
function HandleRow({ card }) {
const handleRef = useRef(null);
return (
<Draggable
containerId="handles"
index={card.index}
item={card}
dragHandle={handleRef}
>
<div className="row">
<button ref={handleRef} aria-label="Drag">≡</button>
<div className="body">{card.title}</div>
</div>
</Draggable>
);
}Native File Drop
Native OS File Drop
<Droppable
containerId="files"
index={0}
acceptFiles
onDrop={(source) => {
const fileList = source.item.files as FileList;
// handle files...
}}
>
<div>Drop files from your desktop here</div>
</Droppable>Import
import { Draggable, Droppable } from 'fluxo-ui';Draggable Props
containerIdreqstringUnique identifier for the container this draggable belongs to
containerIdreqstringUnique identifier for the container this draggable belongs to
indexreqnumberIndex of the item in the container
indexreqnumberIndex of the item in the container
itemreqanyThe actual item data being dragged
itemreqanyThe actual item data being dragged
idstring | numberOptional unique identifier for the item
idstring | numberOptional unique identifier for the item
itemTypestring"'any'"Type of the draggable item (used for drop validation)
itemTypestring"'any'"Type of the draggable item (used for drop validation)
argsanyAdditional arguments to pass along with drag data
argsanyAdditional arguments to pass along with drag data
canDragboolean"true"Whether the item can be dragged
canDragboolean"true"Whether the item can be dragged
onRemove(source: { index: number; id?: string | number }, dropResult: DropResult | null) => voidCallback when item is removed from its original container
onRemove(source: { index: number; id?: string | number }, dropResult: DropResult | null) => voidCallback when item is removed from its original container
onDragStart(item: DragItem, monitor: DragSourceMonitor) => voidCallback when drag starts
onDragStart(item: DragItem, monitor: DragSourceMonitor) => voidCallback when drag starts
onDragEnd(item: DragItem | undefined, monitor: DragSourceMonitor) => voidCallback when drag ends
onDragEnd(item: DragItem | undefined, monitor: DragSourceMonitor) => voidCallback when drag ends
classNamestringAdditional CSS classes
classNamestringAdditional CSS classes
draggingClassNamestringExtra CSS classes applied only while the item is being dragged
draggingClassNamestringExtra CSS classes applied only while the item is being dragged
hideDefaultPreviewboolean"false"Hide the default browser drag preview (useful for custom overlays)
hideDefaultPreviewboolean"false"Hide the default browser drag preview (useful for custom overlays)
childrenReactNode | ((props: DraggableRenderProps) => ReactNode)Children can be ReactNode or render prop function
childrenReactNode | ((props: DraggableRenderProps) => ReactNode)Children can be ReactNode or render prop function
Droppable Props
containerIdreqstringUnique identifier for the container
containerIdreqstringUnique identifier for the container
indexreqnumberIndex position within the container
indexreqnumberIndex position within the container
idstring | numberOptional unique identifier
idstring | numberOptional unique identifier
acceptstring | string[]"'any'"Type(s) of draggable items this droppable accepts
acceptstring | string[]"'any'"Type(s) of draggable items this droppable accepts
argsanyAdditional arguments to pass to drop handler
argsanyAdditional arguments to pass to drop handler
canDropboolean | ((item: DragItem, monitor: DropTargetMonitor) => boolean)"true"Whether dropping is currently allowed
canDropboolean | ((item: DragItem, monitor: DropTargetMonitor) => boolean)"true"Whether dropping is currently allowed
onDrop(source: DragItem, target: DropResult) => voidCallback when an item is dropped
onDrop(source: DragItem, target: DropResult) => voidCallback when an item is dropped
onHover(item: DragItem, monitor: DropTargetMonitor) => voidCallback when a draggable item hovers over this droppable
onHover(item: DragItem, monitor: DropTargetMonitor) => voidCallback when a draggable item hovers over this droppable
classNamestringAdditional CSS classes
classNamestringAdditional CSS classes
dropIndicator'highlight' | 'line' | 'none'"'highlight'"Built-in visual indicator style for drop targets
dropIndicator'highlight' | 'line' | 'none'"'highlight'"Built-in visual indicator style for drop targets
linePosition'start' | 'end'"'start'"Where to render the insertion line when dropIndicator='line'
linePosition'start' | 'end'"'start'"Where to render the insertion line when dropIndicator='line'
orientation'vertical' | 'horizontal'"'vertical'"Direction of the line indicator
orientation'vertical' | 'horizontal'"'vertical'"Direction of the line indicator
childrenReactNode | ((props: DroppableRenderProps) => ReactNode)Children can be ReactNode or render prop function
childrenReactNode | ((props: DroppableRenderProps) => ReactNode)Children can be ReactNode or render prop function
Features
Draggable
Wrap any element to make it draggable with full control over item type and drag callbacks
Droppable
Define drop zones that accept specific item types with hover and drop callbacks
Type-Based Restrictions
Accept only specific item types per drop zone using the accept prop
Render Props
Access isDragging, isOver, canDrop, and ref callbacks via render prop pattern
Multi-Container
Move items between multiple containers for kanban boards and category sorting
onRemove Callback
Cleanly remove items from their source container when dropped elsewhere
Accessibility
Keyboard drag support and ARIA attributes for screen reader compatibility
Theming
Full dark/light + 5 brand themes via CSS variables — zero extra config