|
1 | 1 | # Sortable libraries comparison |
2 | 2 |
|
3 | | -Drag-and-drop reorderable lists are one of the most-requested UI primitives in any |
4 | | -modern component library — and one of the most frequently re-implemented per |
5 | | -framework. The JavaScript ecosystem has converged on a small set of dominant |
6 | | -solutions, but every one of them is either tied to a specific framework or |
7 | | -forces consumers to drop down to imperative engine code at the edges. |
8 | | - |
9 | | -Rozie's `SortableList` (and its `SortableListPair` / `SortableListNested` |
10 | | -siblings) is the killer demo for [Rozie's competitive wedge](/guide/why): |
11 | | -one `.rozie` file compiles to idiomatic React, Vue, Svelte, Angular, Solid, |
12 | | -and Lit consumers — and ships a feature set that every standalone library |
13 | | -on this page either skips entirely or supports partially. |
14 | | - |
15 | | -## Comparison matrix |
16 | | - |
17 | | -| Library | Frameworks | Mouse drag | Keyboard drag | Nested droppables | Cross-list state sync | Custom drag handles | |
18 | | -| ---------------------------------- | --------------------------- | :--------: | :-----------: | :---------------: | :-------------------: | :-----------------: | |
19 | | -| **[Rozie SortableList](/examples/sortable-list)** | **React + Vue + Svelte + Angular + Solid + Lit** | **✓** | **✓** | **✓** | **✓** | **✓** | |
20 | | -| [react-sortablejs](https://github.com/SortableJS/react-sortablejs) | React only | ✓ | ✗ | partial | ✗ | ✓ | |
21 | | -| [react-beautiful-dnd](https://github.com/atlassian/react-beautiful-dnd) | React only | ✓ | ✓ | partial | ✗ | ✓ | |
22 | | -| [dnd-kit](https://dndkit.com/) | React only | ✓ | ✓ | ✓ | partial | ✓ | |
23 | | -| [Vue.Draggable](https://github.com/SortableJS/Vue.Draggable) | Vue only | ✓ | ✗ | partial | partial | ✓ | |
24 | | -| [svelte-dnd-action](https://github.com/isaacHagoel/svelte-dnd-action) | Svelte only | ✓ | ✓ | partial | partial | ✓ | |
25 | | -| [Angular CDK Drag-Drop](https://material.angular.io/cdk/drag-drop) | Angular only | ✓ | ✓ | ✓ | partial | ✓ | |
26 | | - |
27 | | -The matrix scores each library against the feature set that ships out of the |
28 | | -box, without consumer-authored workarounds. "Partial" means the library |
29 | | -exposes lower-level primitives but doesn't provide a turnkey solution for the |
30 | | -column. |
31 | | - |
32 | | -## Why Rozie's row reads ✓ on every column |
33 | | - |
34 | | -- **Mouse drag.** SortableList wraps the battle-tested |
35 | | - [SortableJS](https://sortablejs.github.io/Sortable/) engine; Rozie's per-target |
36 | | - emit reconciles the engine's direct DOM mutation with each framework's |
37 | | - keyed-list reconciler via the |
38 | | - [`$reconcileAfterDomMutation()`](/guide/features#r-external-and-reconcileafterdommutation-—-dom-the-framework-doesn-t-own) |
39 | | - sigil so it doesn't fight on any of the six targets. The |
40 | | - [sortable-drag VR spec](https://github.com/One-Learning-Community/rozie.js/blob/main/tests/visual-regression/specs/sortable-drag.spec.ts) |
41 | | - pins this for every target cell. |
42 | | -- **Keyboard drag.** A Space-lift / ArrowDown-move / Space-drop / Escape-cancel |
43 | | - keymap with aria-live announcements lives in user source — but the |
44 | | - cross-target focus-restoration leak (Svelte / Solid / Lit's keyed |
45 | | - reconcilers re-create row DOM on reorder, dropping focus to `<body>`) is |
46 | | - closed by Rozie's |
47 | | - [`$restoreFocus(selector, idx)`](/guide/features#restorefocus-selector-idx-—-keep-focus-on-a-row-across-keyed-reconciler-re-renders) |
48 | | - sigil. No per-target user-source workarounds needed. |
49 | | -- **Nested droppables.** `SortableListNested` composes `SortableList` with |
50 | | - itself (a Kanban-column demo) and via `KanbanColumn` wrapper, exercising |
51 | | - cross-column card drag with reorderable columns. The same source compiles |
52 | | - to all six targets unchanged. |
53 | | -- **Cross-list state sync.** `SortableListPair` exercises SortableList's |
54 | | - `onAdd` / `onRemove` engine callbacks plus a module-level transfer slot — |
55 | | - dragging an item from list A to list B updates BOTH bound arrays atomically. |
56 | | - No per-framework state-management glue. |
57 | | -- **Custom drag handles.** `:handle="$classSelector('grip')"` resolves on every |
58 | | - target, including React — authored class names render literally everywhere |
59 | | - (React scopes via `[data-rozie-s-<hash>]`, it doesn't hash the class name), and |
60 | | - `$classSelector` lowers to the literal `".grip"` per target while typo-checking |
61 | | - it against your `<style>` at compile time. Most React-side libraries either |
62 | | - skip scoped-CSS support or require consumers to wire selectors around a class |
63 | | - mangler manually. |
64 | | - |
65 | | -## Caveats |
66 | | - |
67 | | -- **The keyboard cell is a feature of Rozie's SortableList example**, not a |
68 | | - property of every library that wraps SortableJS. The keyboard map + |
69 | | - aria-live announcements live in `packages/ui/sortable-list/src/SortableList.rozie`; the |
70 | | - cross-target focus restoration is Rozie's `$restoreFocus` sigil. Library |
71 | | - authors who fork SortableList and want the same keyboard contract get it |
72 | | - for free. |
73 | | -- The matrix scores library out-of-the-box capability. Every "partial" column |
74 | | - is reachable on those libraries with consumer-authored glue, but Rozie's |
75 | | - `✓` means *no consumer glue required*. |
76 | | -- Rozie compiles to six targets — react-beautiful-dnd, dnd-kit, Vue.Draggable, |
77 | | - svelte-dnd-action, and Angular CDK are excellent single-framework choices. |
78 | | - The comparison is about cross-framework reach, not single-framework |
79 | | - ergonomics. |
80 | | - |
81 | | -## Try the live demo |
82 | | - |
83 | | -The [SortableList example page](/examples/sortable-list) lives in the |
84 | | -documentation; the [SortableListDemo source](https://github.com/One-Learning-Community/rozie.js/blob/main/examples/demos/SortableListDemo.rozie) |
85 | | -is the same `.rozie` file that powers every target cell in the matrix above — |
86 | | -including the screen-reader-driven keyboard contract. |
87 | | - |
88 | | -Ready to ship it? The [`SortableList` showcase + API reference](/guide/sortable-list) |
89 | | -documents the `@rozie-ui/sortable-list-*` packages — one pre-compiled, |
90 | | -per-framework install (`npm i @rozie-ui/sortable-list-react`, etc.) with no |
91 | | -Rozie toolchain required. |
| 3 | +How `@rozie-ui/sortable-list` compares to the per-framework drag-and-drop reorderable-list ecosystem. Unlike the date-picker or rich-text ecosystems, this is **not** a single-engine landscape: some libraries are thin bindings over the [SortableJS](https://sortablejs.github.io/Sortable/) engine (react-sortablejs, vuedraggable, ngx-sortablejs — the family Rozie joins), while others are full native drag-and-drop toolkits with their *own* engines (dnd-kit, Angular CDK, svelte-dnd-action, solid-dnd). So the comparison spans two axes at once: **engine** (SortableJS vs bespoke) and **framework reach**. Rozie wraps SortableJS and ships one source to all six frameworks. |
| 4 | + |
| 5 | +> Research snapshot: 2026-06-07. Versions and download counts move; treat them as of that date. Weekly-download figures are an npm snapshot for the window 2026-05-27→06-02 — a popularity datum, *not* a quality verdict. |
| 6 | +
|
| 7 | +## The libraries at a glance |
| 8 | + |
| 9 | +| Library | Engine | Frameworks | Latest | Weekly downloads | Maintenance | Key capability | |
| 10 | +| --- | --- | --- | --- | --- | --- | --- | |
| 11 | +| **[react-sortablejs](https://github.com/SortableJS/react-sortablejs)** | SortableJS | React | 6.1.4 | ~364k | **last published ~4 yr ago** | Same engine as Rozie; thin React binding | |
| 12 | +| **[@dnd-kit/core](https://dndkit.com/)** | own | React | 6.3.1 | ~17.0M | active (v6.3.1, ~2 yr) | Modern React leader — sensors, virtualization, a11y | |
| 13 | +| **[react-beautiful-dnd](https://github.com/atlassian/react-beautiful-dnd)** | own | React | 13.1.1 | ~2.32M | **deprecated; repo archived 2025-08** | Still widely used; **no React 19** | |
| 14 | +| **[vuedraggable](https://github.com/SortableJS/vue.draggable.next)** | SortableJS | Vue 3 | 4.1.0 | ~1.35M | stale (maintained alt: vue-draggable-plus) | `v-model` array + SortableJS | |
| 15 | +| **[svelte-dnd-action](https://github.com/isaacHagoel/svelte-dnd-action)** | own | Svelte | 0.9.69 | ~142k | active; **Svelte 5** | Action-based, FLIP animations, keyboard + a11y | |
| 16 | +| **[@angular/cdk](https://material.angular.dev/cdk/drag-drop)** (drag-drop) | own | Angular | 21.x | ~3.73M¹ | active (first-party) | Connected lists, keyboard, `moveItemInArray` | |
| 17 | +| **[@thisbeyond/solid-dnd](https://github.com/thisbeyond/solid-dnd)** | own | Solid | 0.7.5 | ~54k | **last published ~2 yr ago** | Solid primitives toolkit | |
| 18 | +| **Lit** | — | — | — | — | — | ❌ no idiomatic DnD-list component | |
| 19 | +| **Rozie** | SortableJS | **6** | 0.1.0 | — | this repo (2026-06) | One source → six idiomatic packages | |
| 20 | + |
| 21 | +¹ `@angular/cdk` downloads are for the whole CDK; the drag-drop module is one entry point within it. The SortableJS engine itself (`sortablejs`) is at `1.15.7`, ~3.73M/wk, and is actively maintained — the engine is healthy; the per-framework **bindings** are the uneven part. |
| 22 | + |
| 23 | +The wedge here is different in shape from the other engine ports. Every framework *does* have a strong drag-and-drop option — but they are **different engines with different APIs**, the SortableJS bindings specifically are stale (react-sortablejs ~4 yr, vuedraggable's Vue-3 line maintenance-flagged), the most-loved React option (react-beautiful-dnd) is **deprecated and archived**, **Solid's `@thisbeyond/solid-dnd` hasn't shipped in ~2 years**, and **Lit has no idiomatic reorderable-list component at all**. Standardizing one reorderable-list *contract* across all six frameworks means learning six different libraries — or compiling one `.rozie` source. |
| 24 | + |
| 25 | +## Feature matrix |
| 26 | + |
| 27 | +Per-framework column = the de-facto leader for that framework (React = `@dnd-kit`, the modern standard; the same-engine React binding is `react-sortablejs`, see ²). Cell legend: **✅** out-of-the-box · **❌** not supported · **~** partial / consumer-glue-required. |
| 28 | + |
| 29 | +| Capability | `@dnd-kit`² (React) | `vuedraggable` (Vue) | `svelte-dnd-action` (Svelte) | `@angular/cdk` (Angular) | `@thisbeyond/solid-dnd` (Solid) | Lit (none) | **`@rozie-ui/sortable-list`** | |
| 30 | +| --- | :---: | :---: | :---: | :---: | :---: | :---: | :---: | |
| 31 | +| Render + reorder a list | ✅ | ✅ | ✅ | ✅ | ✅ | hand-roll | ✅ | |
| 32 | +| Pointer / mouse drag | ✅ | ✅ | ✅ | ✅ | ✅ | hand-roll | ✅ | |
| 33 | +| **Keyboard drag + a11y live-region** | ✅ | ❌ | ✅ | ✅ | ~ | hand-roll | ✅³ | |
| 34 | +| Nested / cross-list transfer | ✅ | ~ (put/pull) | ✅ | ✅ (connected) | ~ | hand-roll | ✅⁴ | |
| 35 | +| **Two-way bound data array** | ❌⁵ | ✅ (`v-model`) | ~ (consider/finalize) | ❌⁵ | ❌⁵ | hand-roll | ✅ `r-model:items` | |
| 36 | +| Custom drag handle | ✅ | ✅ | ✅ | ✅ | ~ | hand-roll | ✅ `$classSelector` | |
| 37 | +| Framework-native per-row slot/render | ✅ | ✅ | ✅ | ✅ | ✅ | hand-roll | ✅ scoped slot | |
| 38 | +| Imperative handle | ~ (context/sensors) | ~ (instance) | ~ | ~ (`CdkDropList`) | ~ | hand-roll | ❌⁶ | |
| 39 | +| Latest-framework support | React 19 | Vue 3 | **Svelte 5** | Angular 21 | Solid 1.x (stale) | — | R18+/V3.4+/Sv5/Ng19+/Solid/Lit | |
| 40 | +| Actively maintained | ✅ (~2 yr cadence) | ~ | ✅ | ✅ | ❌ | — | ✅ | |
| 41 | +| One source → all 6 frameworks | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | |
| 42 | + |
| 43 | +² **The React story is split three ways.** `@dnd-kit` (~17.0M/wk) is the modern React leader (own engine — sensors, virtualization-friendly, accessible); `react-beautiful-dnd` (~2.32M/wk) is **deprecated and archived** (no React 19); and `react-sortablejs` (~364k/wk, **~4 yr since last publish**) is the same-SortableJS-engine binding Rozie is the direct peer of. Rozie's React leaf is current and wraps SortableJS like react-sortablejs, but ships the keyboard / a11y / two-way contract react-sortablejs lacks. |
| 44 | + |
| 45 | +³ **Keyboard drag is a feature of Rozie's `SortableList` source**, not of SortableJS: Space lifts / drops, ArrowUp / ArrowDown move, Escape cancels, Enter is an alternate drop — with `aria-live` announcements. The cross-target focus-restoration leak (Svelte / Solid / Lit keyed reconcilers re-create row DOM on reorder, dropping focus to `<body>`) is closed by Rozie's [`$restoreFocus`](/guide/features#restorefocus-selector-idx-—-keep-focus-on-a-row-across-keyed-reconciler-re-renders) sigil. `react-sortablejs` and `vuedraggable` ship no keyboard contract. |
| 46 | + |
| 47 | +⁴ Nested + cross-list flows are shown by the `SortableListNested` / `KanbanColumn` and `SortableListPair` siblings — cross-column card drag with reorderable columns, and atomic A→B transfer across two bound arrays via SortableJS's `group` / clone modes and the `onAdd` / `onRemove` callbacks — from the same source on all six targets. |
| 48 | + |
| 49 | +⁵ **No two-way data binding.** `@dnd-kit`, `@angular/cdk`, and `@thisbeyond/solid-dnd` hand you a drag-end event (`onDragEnd` / `cdkDropListDropped` / drag store) and you mutate state yourself (CDK ships a `moveItemInArray` helper, but you call it). `vuedraggable` is the exception with real `v-model`. Rozie gives every target a two-way `items` array — pass an array, get a reordered array back, no manual `onChange → setState` wiring. |
| 50 | + |
| 51 | +⁶ **Rozie's `SortableList` has no `$expose` imperative handle** — it predates the Phase-21 `$expose` capability that every other `@rozie-ui` port ships. See [G1](#gap-status-what-shipped-what-s-still-deferred). |
| 52 | + |
| 53 | +## Where Rozie wins today |
| 54 | + |
| 55 | +- **One definition, six idiomatic packages** — including the two frameworks the ecosystem underserves: **Solid (`@thisbeyond/solid-dnd` ~54k/wk, ~2 yr stale)** and **Lit (no idiomatic reorderable-list component exists)**. A Lit dev today hand-rolls SortableJS over their own DOM; a Solid dev reaches for a toolkit that hasn't shipped since 2024. |
| 56 | +- **Keyboard drag + screen-reader announcements built into the source** — Space / Arrow / Escape / Enter with `aria-live`, and the cross-framework focus-restoration leak closed by [`$restoreFocus`](/guide/features#restorefocus-selector-idx-—-keep-focus-on-a-row-across-keyed-reconciler-re-renders). dnd-kit, CDK, and svelte-dnd-action have keyboard stories too — but each is per-framework; react-sortablejs and vuedraggable have none. |
| 57 | +- **Two-way bound `items` array** (`r-model:items`) on all six — the thing every dnd-kit / CDK / solid-dnd consumer wires by hand. Pass an array, render rows through the scoped default slot, get the reordered array back. |
| 58 | +- **Cross-list sync + nesting from one source** — `SortableListPair` (atomic transfer across two bound arrays) and `SortableListNested` / `KanbanColumn` (reorderable columns of reorderable cards), the same `.rozie` compiled to all six targets. |
| 59 | +- **Custom drag handles via `$classSelector`** — resolves on every target including React's scoped-CSS (authored class names render literally; `$classSelector` lowers to the literal `".grip"` per target and typo-checks it against your `<style>` at compile time). |
| 60 | +- **The hard part solved once** — the SortableJS-direct-DOM-mutation-vs-framework-reconciler dance (the reason these wrappers exist at all) is encapsulated in `useSortableJS()` plus the [`$reconcileAfterDomMutation()`](/guide/features#r-external-and-reconcileafterdommutation-—-dom-the-framework-doesn-t-own) sigil, hardened against SortableJS's fragile fallback-mode event shapes, across all six keyed reconcilers. |
| 61 | + |
| 62 | +The ✅ cells in Rozie's row are pinned per target by the [sortable-drag VR spec](https://github.com/One-Learning-Community/rozie.js/blob/main/tests/visual-regression/specs/sortable-drag.spec.ts) — which measures *Rozie's* behavior across targets and says nothing measured about the competitors' behavior. |
| 63 | + |
| 64 | +## Gap status — what shipped, what's still deferred {#gap-status-what-shipped-what-s-still-deferred} |
| 65 | + |
| 66 | +This page concedes where the standalone libraries are genuinely ahead — that's what keeps the comparison credible, and it doubles as Rozie's own roadmap. |
| 67 | + |
| 68 | +| Gap | Who has it | Severity | Rozie status | |
| 69 | +| --- | --- | --- | --- | |
| 70 | +| **G1 — Imperative `$expose` handle** | `@dnd-kit` (sensors/context), CDK (`CdkDropList`) | **Medium** | **⏳ Deferred** — `SortableList` predates the Phase-21 `$expose` capability, so there is no uniform handle to reach the SortableJS instance or trigger a programmatic sort. Every other `@rozie-ui` port ships `$expose`; this is the holdout. | |
| 71 | +| G2 — Live reconcile of construction-time knobs | (engine-level) | Low | **⏳ Deferred (by design)** — `forceFallback` / `swapThreshold` / `cloneable` are construction-time-only SortableJS knobs; changing them at runtime requires re-keying the component (the [documented re-mount pattern](/guide/sortable-list#remount-on-construction-time-only-changes)). The runtime-updatable props *are* live-reconciled via `instance.option()`. | |
| 72 | +| G3 — List virtualization for large datasets | `@dnd-kit` (+ virtualizers) | Medium | **⏳ Deferred** — SortableJS renders all rows; very large lists want windowing. dnd-kit composes with `@tanstack/virtual`; Rozie has no virtualization story yet. | |
| 73 | +| G4 — Multi-select / multi-drag | `@dnd-kit`, SortableJS MultiDrag plugin | Low | **⏳ Deferred** — SortableJS's `MultiDrag` plugin is not mounted, so dragging multiple rows at once isn't wired. (Plain SortableJS options pass through via `:options`; plugins need a mount Rozie doesn't yet bridge.) | |
| 74 | +| G5 — Spring / FLIP reorder animation | `svelte-dnd-action`, `@dnd-kit` | Low | **⏳ Deferred** — animation is SortableJS's `animation` (ms) + `easing` only; no FLIP/spring-grade reorder choreography. | |
| 75 | + |
| 76 | +## Honest caveats |
| 77 | + |
| 78 | +- **Modern React leans dnd-kit, not SortableJS.** `@dnd-kit` (~17.0M/wk) is the React drag-and-drop standard in 2026 — sensors, virtualization, a rich ecosystem — and `react-beautiful-dnd` (~2.32M/wk), though deprecated/archived, is still everywhere. Rozie wraps **SortableJS**, a different engine with a simpler DOM-mutation model and its own tradeoffs. For a single-React app that needs virtualization or dnd-kit's sensor model, dnd-kit is the better pick. Rozie's value is cross-framework reach plus the keyboard / a11y / two-way contract from one source — not "better than dnd-kit on React." |
| 79 | +- **No imperative handle yet (G1).** This is the one place `@rozie-ui/sortable-list` is behind its own sibling ports; it's the top roadmap item for this component. |
| 80 | +- **Angular CDK and svelte-dnd-action are first-rate native toolkits.** CDK (first-party, connected lists, keyboard) and svelte-dnd-action (FLIP animations, Svelte 5, actively maintained) are excellent *single-framework* choices. The matrix scores cross-framework reach, not single-framework ergonomics. |
| 81 | +- **`@rozie-ui/sortable-list` is `0.1.0`.** The surface (17 props / 5 events / scoped row slot) is stable and VR-pinned, but younger than the established libraries — and it inherits SortableJS's engine-level limitations (touch-fallback fragility, no windowing) along with its strengths. |
| 82 | + |
| 83 | +## Cross-references |
| 84 | + |
| 85 | +- [`SortableList` showcase & API](/guide/sortable-list) — the full surface, quick starts, recipes, and the `SortableListPair` / `SortableListNested` / `KanbanColumn` siblings. |
| 86 | +- [SortableList example & output](/examples/sortable-list) — the live demo with per-target compiled output side by side. |
| 87 | +- [`SortableList.rozie` source](https://github.com/One-Learning-Community/rozie.js/blob/main/packages/ui/sortable-list/src/SortableList.rozie) and the [`useSortableJS()` bridge](https://github.com/One-Learning-Community/rozie.js/blob/main/packages/ui/sortable-list/src/internal/useSortableJS.ts). |
| 88 | +- [`$restoreFocus()`](/guide/features#restorefocus-selector-idx-—-keep-focus-on-a-row-across-keyed-reconciler-re-renders) · [`$reconcileAfterDomMutation()`](/guide/features#r-external-and-reconcileafterdommutation-—-dom-the-framework-doesn-t-own) · [`$classSelector()`](/guide/features#classselector-—-handing-a-class-name-to-a-vanilla-js-engine) — the sigils that make the cross-framework SortableJS bridge work. |
0 commit comments