Fluxo UIFluxo UIv0.1.1

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.

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

1. First Item
2. Second Item
3. Third Item
4. Fourth Item
5. Fifth Item
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

#1

Design UI mockups

Create wireframes and mockups

#2

Implement features

Build core functionality

#3

Write tests

Add unit and integration tests

#4

Deploy to production

Configure CI/CD and deploy

#5
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

👀Review pull requests
📝Update documentation
🐛Fix bug in login
Drop here

In Progress

Implement dark mode
Drop here

Done

⚙️Setup CI/CD pipeline
Drop here
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

User Authentication
Dark Mode Toggle
File Upload

Bugs

🐛Memory Leak in Dashboard
🐛Form Validation Error

Mixed (Accepts Both)

Drop features or bugs here

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

First Item
#1
Second Item
#2
Third Item
#3
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)

500 rows · first: Row #1 · last: Row #500
Row #1
Row #2
Row #3
Row #4
Row #5
Row #6
Row #7
Row #8
Row #9
Row #10
Row #11
Row #12
Row #13
Row #14
Row #15
Row #16
Row #17
Row #18
Row #19
Row #20
Row #21
Row #22
Row #23
Row #24
Row #25
Row #26
Row #27
Row #28
Row #29
Row #30
Row #31
Row #32
Row #33
Row #34
Row #35
Row #36
Row #37
Row #38
Row #39
Row #40
Row #41
Row #42
Row #43
Row #44
Row #45
Row #46
Row #47
Row #48
Row #49
Row #50
Row #51
Row #52
Row #53
Row #54
Row #55
Row #56
Row #57
Row #58
Row #59
Row #60
Row #61
Row #62
Row #63
Row #64
Row #65
Row #66
Row #67
Row #68
Row #69
Row #70
Row #71
Row #72
Row #73
Row #74
Row #75
Row #76
Row #77
Row #78
Row #79
Row #80
Row #81
Row #82
Row #83
Row #84
Row #85
Row #86
Row #87
Row #88
Row #89
Row #90
Row #91
Row #92
Row #93
Row #94
Row #95
Row #96
Row #97
Row #98
Row #99
Row #100
Row #101
Row #102
Row #103
Row #104
Row #105
Row #106
Row #107
Row #108
Row #109
Row #110
Row #111
Row #112
Row #113
Row #114
Row #115
Row #116
Row #117
Row #118
Row #119
Row #120
Row #121
Row #122
Row #123
Row #124
Row #125
Row #126
Row #127
Row #128
Row #129
Row #130
Row #131
Row #132
Row #133
Row #134
Row #135
Row #136
Row #137
Row #138
Row #139
Row #140
Row #141
Row #142
Row #143
Row #144
Row #145
Row #146
Row #147
Row #148
Row #149
Row #150
Row #151
Row #152
Row #153
Row #154
Row #155
Row #156
Row #157
Row #158
Row #159
Row #160
Row #161
Row #162
Row #163
Row #164
Row #165
Row #166
Row #167
Row #168
Row #169
Row #170
Row #171
Row #172
Row #173
Row #174
Row #175
Row #176
Row #177
Row #178
Row #179
Row #180
Row #181
Row #182
Row #183
Row #184
Row #185
Row #186
Row #187
Row #188
Row #189
Row #190
Row #191
Row #192
Row #193
Row #194
Row #195
Row #196
Row #197
Row #198
Row #199
Row #200
Row #201
Row #202
Row #203
Row #204
Row #205
Row #206
Row #207
Row #208
Row #209
Row #210
Row #211
Row #212
Row #213
Row #214
Row #215
Row #216
Row #217
Row #218
Row #219
Row #220
Row #221
Row #222
Row #223
Row #224
Row #225
Row #226
Row #227
Row #228
Row #229
Row #230
Row #231
Row #232
Row #233
Row #234
Row #235
Row #236
Row #237
Row #238
Row #239
Row #240
Row #241
Row #242
Row #243
Row #244
Row #245
Row #246
Row #247
Row #248
Row #249
Row #250
Row #251
Row #252
Row #253
Row #254
Row #255
Row #256
Row #257
Row #258
Row #259
Row #260
Row #261
Row #262
Row #263
Row #264
Row #265
Row #266
Row #267
Row #268
Row #269
Row #270
Row #271
Row #272
Row #273
Row #274
Row #275
Row #276
Row #277
Row #278
Row #279
Row #280
Row #281
Row #282
Row #283
Row #284
Row #285
Row #286
Row #287
Row #288
Row #289
Row #290
Row #291
Row #292
Row #293
Row #294
Row #295
Row #296
Row #297
Row #298
Row #299
Row #300
Row #301
Row #302
Row #303
Row #304
Row #305
Row #306
Row #307
Row #308
Row #309
Row #310
Row #311
Row #312
Row #313
Row #314
Row #315
Row #316
Row #317
Row #318
Row #319
Row #320
Row #321
Row #322
Row #323
Row #324
Row #325
Row #326
Row #327
Row #328
Row #329
Row #330
Row #331
Row #332
Row #333
Row #334
Row #335
Row #336
Row #337
Row #338
Row #339
Row #340
Row #341
Row #342
Row #343
Row #344
Row #345
Row #346
Row #347
Row #348
Row #349
Row #350
Row #351
Row #352
Row #353
Row #354
Row #355
Row #356
Row #357
Row #358
Row #359
Row #360
Row #361
Row #362
Row #363
Row #364
Row #365
Row #366
Row #367
Row #368
Row #369
Row #370
Row #371
Row #372
Row #373
Row #374
Row #375
Row #376
Row #377
Row #378
Row #379
Row #380
Row #381
Row #382
Row #383
Row #384
Row #385
Row #386
Row #387
Row #388
Row #389
Row #390
Row #391
Row #392
Row #393
Row #394
Row #395
Row #396
Row #397
Row #398
Row #399
Row #400
Row #401
Row #402
Row #403
Row #404
Row #405
Row #406
Row #407
Row #408
Row #409
Row #410
Row #411
Row #412
Row #413
Row #414
Row #415
Row #416
Row #417
Row #418
Row #419
Row #420
Row #421
Row #422
Row #423
Row #424
Row #425
Row #426
Row #427
Row #428
Row #429
Row #430
Row #431
Row #432
Row #433
Row #434
Row #435
Row #436
Row #437
Row #438
Row #439
Row #440
Row #441
Row #442
Row #443
Row #444
Row #445
Row #446
Row #447
Row #448
Row #449
Row #450
Row #451
Row #452
Row #453
Row #454
Row #455
Row #456
Row #457
Row #458
Row #459
Row #460
Row #461
Row #462
Row #463
Row #464
Row #465
Row #466
Row #467
Row #468
Row #469
Row #470
Row #471
Row #472
Row #473
Row #474
Row #475
Row #476
Row #477
Row #478
Row #479
Row #480
Row #481
Row #482
Row #483
Row #484
Row #485
Row #486
Row #487
Row #488
Row #489
Row #490
Row #491
Row #492
Row #493
Row #494
Row #495
Row #496
Row #497
Row #498
Row #499
Row #500

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

itemsreq
T[]

Array of items to render

accept
string | string[]

Type(s) of external draggable items this sortable accepts

itemType
string"'any'"

Default item type for items within this sortable

itemTypeProp
string

Property name on items to get their type (for mixed item types)

args
any

Additional arguments passed to callbacks

allowRemove
boolean"false"

Auto-remove items from source list on cross-container drop (default false — manage removal in destination onDrop)

showPlaceholder
boolean"false"

Whether to show a placeholder drop zone at the end

placeholder
ReactNode

Custom placeholder content

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

gap
string"'0.5rem'"

Gap between items (any valid CSS length)

as
ElementType"'div'"

HTML tag name for the container element

provideDropRef
boolean"false"

Pass drop ref to children render function

provideDragRef
boolean"false"

Pass drag ref to children render function

onChangereq
(items: T[], args?: any, event?: SortableChangeEvent) => void

Callback when items are reordered or changed

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

Callback when an external item is dropped

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

Callback when an item is removed (dragged out)

className
string

Additional CSS classes

childrenreq
(item: T, index: number, refs: { draggable?: DraggableRenderProps; droppable?: DroppableRenderProps }) => ReactNode

Render 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