Sortable
A powerful sortable list component that combines dragging and dropping for easy reordering.
Setup
Sortable is available from the main fluxo-ui entry — no provider wrapping and no extra peer dependencies.
import { Sortable } from 'fluxo-ui';
function MyList() {
const [items, setItems] = useState(['One', 'Two', 'Three']);
return (
<Sortable items={items} onChange={setItems}>
{(item) => <div className="row">{item}</div>}
</Sortable>
);
}Sortable ships with scroll-aware positioning, auto-scroll near container edges, touch and pen support, optional drag handles, delay activation, and fine-grained canDragItem / canDropItem callbacks — all in the main library bundle.
Basic Sortable List
Simple List Reordering
import { Sortable } from 'fluxo-ui';
function SortableList() {
const [items, setItems] = useState([
'First Item',
'Second Item',
'Third Item',
'Fourth Item',
]);
return (
<Sortable
items={items}
onChange={(newItems) => setItems(newItems)}
dropIndicator="line"
>
{(item, index) => (
<div className="bg-blue-600 px-4 py-3 rounded-md">
{index + 1}. {item}
</div>
)}
</Sortable>
);
}Complex Items with Details
Task List with Priority
Setup project
Initialize repository and install dependencies
Design UI mockups
Create wireframes and mockups
Implement features
Build core functionality
Write tests
Add unit and integration tests
Deploy to production
Configure CI/CD and deploy
import { Sortable } from 'fluxo-ui';
interface Task {
id: number;
title: string;
description?: string;
priority?: 'low' | 'medium' | 'high';
}
function TaskList() {
const [tasks, setTasks] = useState<Task[]>([
{ id: 1, title: 'Setup project', priority: 'high' },
{ id: 2, title: 'Design UI', priority: 'medium' },
{ id: 3, title: 'Deploy', priority: 'low' },
]);
return (
<Sortable
items={tasks}
onChange={(newTasks) => setTasks(newTasks)}
className="space-y-3"
>
{(task) => (
<div className="bg-gray-100 rounded-lg p-4 cursor-move">
<h3>{task.title}</h3>
<span className={`priority-${task.priority}`}>
{task.priority}
</span>
</div>
)}
</Sortable>
);
}Multiple Sortable Lists
Kanban Board with Sortable Columns
To Do
In Progress
Done
import { Sortable } from 'fluxo-ui';
function KanbanBoard() {
const [columns, setColumns] = useState({
todo: [{ id: 1, text: 'Task 1' }],
inProgress: [{ id: 2, text: 'Task 2' }],
done: [{ id: 3, text: 'Task 3' }],
});
const handleDrop = (columnKey, source, target) => {
if (source.containerId !== target.containerId) {
const sourceKey = Object.keys(columns).find(
key => columns[key].some(item => item.id === source.item.id)
);
setColumns({
...columns,
[sourceKey]: columns[sourceKey].filter(
item => item.id !== source.item.id
),
[columnKey]: [...columns[columnKey], source.item],
});
}
};
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{Object.keys(columns).map(columnKey => (
<Sortable
key={columnKey}
items={columns[columnKey]}
onChange={(newItems) =>
setColumns({ ...columns, [columnKey]: newItems })
}
onDrop={(source, target) =>
handleDrop(columnKey, source, target)
}
showPlaceholder
>
{(item) => <div>{item.text}</div>}
</Sortable>
))}
</div>
);
}Type-Based Drag & Drop Restrictions
Accept Only Specific Types
Features
Bugs
Mixed (Accepts Both)
Try dragging items between columns. The "Mixed" column accepts both features and bugs, but features and bugs columns only accept their own types.
import { Sortable } from 'fluxo-ui';
function TypeBasedSorting() {
const [items, setItems] = useState({
features: [
{ id: 1, text: 'User Auth', type: 'feature' },
{ id: 2, text: 'Dark Mode', type: 'feature' },
],
bugs: [
{ id: 3, text: 'Memory Leak', type: 'bug' },
],
mixed: [],
});
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* Features - only accepts feature type */}
<Sortable
items={items.features}
onChange={(newItems) =>
setItems(prev => ({ ...prev, features: newItems }))
}
itemType="feature"
>
{(item) => <div>{item.text}</div>}
</Sortable>
{/* Bugs - only accepts bug type */}
<Sortable
items={items.bugs}
onChange={(newItems) =>
setItems(prev => ({ ...prev, bugs: newItems }))
}
itemType="bug"
>
{(item) => <div>{item.text}</div>}
</Sortable>
{/* Mixed - accepts both types */}
<Sortable
items={items.mixed}
accept={['feature', 'bug']}
onChange={(newItems) =>
setItems(prev => ({ ...prev, mixed: newItems }))
}
onDrop={(source) => {
setItems(prev => {
const newState = { ...prev };
if (source.itemType === 'feature') {
newState.features = newState.features.filter(
item => item.id !== source.item.id
);
} else if (source.itemType === 'bug') {
newState.bugs = newState.bugs.filter(
item => item.id !== source.item.id
);
}
newState.mixed = [...newState.mixed, source.item];
return newState;
});
}}
>
{(item) => <div>{item.text}</div>}
</Sortable>
</div>
);
}Custom Drag Handles
Using provideDragRef for Custom Handles
import { Sortable } from 'fluxo-ui';
function ListWithHandles() {
const [items, setItems] = useState(['Item 1', 'Item 2', 'Item 3']);
return (
<Sortable
items={items}
onChange={setItems}
provideDragRef
>
{(item, index, { draggable }) => (
<div className="flex items-center gap-3">
{/* Custom drag handle */}
<div ref={draggable?.dragRef} className="cursor-move">
⋮⋮
</div>
<div className="flex-1">{item}</div>
</div>
)}
</Sortable>
);
}Scrollable Container + Long List
Scrollable Container + Long List (500 items)
Drag a row near the top or bottom edge of the container — it auto-scrolls. Scroll the container while holding a drag, or scroll the page — the insertion line stays locked to where the item will actually land.
// Scroll-aware Sortable: 500 items inside a 320px-tall scrollable container.
// Positioning is computed from viewport coordinates + getBoundingClientRect,
// so it remains correct at any scroll position. Auto-scroll engages near edges.
const [items, setItems] = useState(() => makeRows(500));
<div style={{ height: 320, overflow: 'auto' }}>
<Sortable items={items} onChange={setItems} idProp="id">
{(row) => <div style={{ background: row.color }}>{row.label}</div>}
</Sortable>
</div>Import
import { Sortable } from 'fluxo-ui';Props
itemsreqT[]Array of items to render
itemsreqT[]Array of items to render
acceptstring | string[]Type(s) of external draggable items this sortable accepts
acceptstring | string[]Type(s) of external draggable items this sortable accepts
itemTypestring"'any'"Default item type for items within this sortable
itemTypestring"'any'"Default item type for items within this sortable
itemTypePropstringProperty name on items to get their type (for mixed item types)
itemTypePropstringProperty name on items to get their type (for mixed item types)
argsanyAdditional arguments passed to callbacks
argsanyAdditional arguments passed to callbacks
allowRemoveboolean"false"Auto-remove items from source list on cross-container drop (default false — manage removal in destination onDrop)
allowRemoveboolean"false"Auto-remove items from source list on cross-container drop (default false — manage removal in destination onDrop)
showPlaceholderboolean"false"Whether to show a placeholder drop zone at the end
showPlaceholderboolean"false"Whether to show a placeholder drop zone at the end
placeholderReactNodeCustom placeholder content
placeholderReactNodeCustom placeholder content
dropIndicator'highlight' | 'line' | 'none'"'line'"Drop indicator style — 'line' shows an insertion line between items, 'highlight' glows the slot
dropIndicator'highlight' | 'line' | 'none'"'line'"Drop indicator style — 'line' shows an insertion line between items, 'highlight' glows the slot
orientation'vertical' | 'horizontal'"'vertical'"Layout direction for items
orientation'vertical' | 'horizontal'"'vertical'"Layout direction for items
gapstring"'0.5rem'"Gap between items (any valid CSS length)
gapstring"'0.5rem'"Gap between items (any valid CSS length)
asElementType"'div'"HTML tag name for the container element
asElementType"'div'"HTML tag name for the container element
provideDropRefboolean"false"Pass drop ref to children render function
provideDropRefboolean"false"Pass drop ref to children render function
provideDragRefboolean"false"Pass drag ref to children render function
provideDragRefboolean"false"Pass drag ref to children render function
onChangereq(items: T[], args?: any, event?: SortableChangeEvent) => voidCallback when items are reordered or changed
onChangereq(items: T[], args?: any, event?: SortableChangeEvent) => voidCallback when items are reordered or changed
onDrop(source: DragItem, target: DropResult, args?: any) => voidCallback when an external item is dropped
onDrop(source: DragItem, target: DropResult, args?: any) => voidCallback when an external item is dropped
onRemove(removed: { index: number; id?: string | number }, dropResult: DropResult | null) => voidCallback when an item is removed (dragged out)
onRemove(removed: { index: number; id?: string | number }, dropResult: DropResult | null) => voidCallback when an item is removed (dragged out)
classNamestringAdditional CSS classes
classNamestringAdditional CSS classes
childrenreq(item: T, index: number, refs: { draggable?: DraggableRenderProps; droppable?: DroppableRenderProps }) => ReactNodeRender function for each item
childrenreq(item: T, index: number, refs: { draggable?: DraggableRenderProps; droppable?: DroppableRenderProps }) => ReactNodeRender function for each item
Features
Reordering
Drag items within a list to reorder them with smooth visual feedback
Multi-List Transfer
Move items between multiple Sortable lists for kanban-style boards
Type Restrictions
Restrict which item types each Sortable accepts using itemType and accept props
Custom Drag Handles
Use provideDragRef to attach the drag ref to a specific handle element
Placeholder Drop Zone
Show a placeholder at the end of the list to indicate the drop target
Render Props
Full access to dragging state for per-item visual feedback via render function
Accessibility
Keyboard drag support and ARIA attributes for screen reader compatibility
Theming
Full dark/light + 5 brand themes via CSS variables — zero extra config