Fluxo UIFluxo UIv0.1.1

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.

Use it directly
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

Item 1
Item 2
Item 3
Item 4

Drop Zone

Drop items here
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

Task A
Task B
Task C

Done

Drop completed tasks here
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

Feature 1
Feature 2
Feature 3

Selected Features

Drag features here
<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

🖼️photo1.jpg
🖼️photo2.png
🖼️photo3.gif

Documents

📄report.pdf
📄notes.docx

Image Folder (Images Only)

Drop images here

Document Folder (Documents Only)

Drop documents here

Recycling Bin (Accepts All)

🗑️
Drop any file here

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)

Alpha
Bravo
Charlie
Delta
Echo

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)

Install grip handles
Only the grip on the left starts a drag.
Body is interactive
You can still click buttons and select text in the body.
Touch friendly
Works the same on mouse, touch, and pen.

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

📁
Drop files from your desktop here
Accepts any file type — uses native OS drag
<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

containerIdreq
string

Unique identifier for the container this draggable belongs to

indexreq
number

Index of the item in the container

itemreq
any

The actual item data being dragged

id
string | number

Optional unique identifier for the item

itemType
string"'any'"

Type of the draggable item (used for drop validation)

args
any

Additional arguments to pass along with drag data

canDrag
boolean"true"

Whether the item can be dragged

onRemove
(source: { index: number; id?: string | number }, dropResult: DropResult | null) => void

Callback when item is removed from its original container

onDragStart
(item: DragItem, monitor: DragSourceMonitor) => void

Callback when drag starts

onDragEnd
(item: DragItem | undefined, monitor: DragSourceMonitor) => void

Callback when drag ends

className
string

Additional CSS classes

draggingClassName
string

Extra CSS classes applied only while the item is being dragged

hideDefaultPreview
boolean"false"

Hide the default browser drag preview (useful for custom overlays)

children
ReactNode | ((props: DraggableRenderProps) => ReactNode)

Children can be ReactNode or render prop function

Droppable Props

containerIdreq
string

Unique identifier for the container

indexreq
number

Index position within the container

id
string | number

Optional unique identifier

accept
string | string[]"'any'"

Type(s) of draggable items this droppable accepts

args
any

Additional arguments to pass to drop handler

canDrop
boolean | ((item: DragItem, monitor: DropTargetMonitor) => boolean)"true"

Whether dropping is currently allowed

onDrop
(source: DragItem, target: DropResult) => void

Callback when an item is dropped

onHover
(item: DragItem, monitor: DropTargetMonitor) => void

Callback when a draggable item hovers over this droppable

className
string

Additional CSS classes

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'

orientation
'vertical' | 'horizontal'"'vertical'"

Direction of the line indicator

children
ReactNode | ((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