Skip to content

Commit 8cbd863

Browse files
serpentbladeclaude
andcommitted
docs(sortable): upgrade comparison page to the standard
Bring sortable-comparison.md to the codemirror/tiptap bar (flagged substandard in the 260606-nt3 sweep: no dated snapshot, no version or download data). Fresh research (snapshot 2026-06-07, npm registry + downloads API): libraries-at-a-glance table with engine/version/downloads/ maintenance, footnoted feature matrix (per-framework leader + Rozie), 'Where Rozie wins today', a G1-G5 gap table, and honest caveats. Honest about the heterogeneous landscape: SortableJS bindings (react-sortablejs ~4yr stale, vuedraggable) vs native toolkits (dnd-kit ~17M/wk modern React leader, Angular CDK, svelte-dnd-action), with react-beautiful-dnd deprecated/archived, @thisbeyond/solid-dnd ~2yr stale, and Lit having no idiomatic component. G1 names our own gap honestly: SortableList has no $expose handle (predates Phase 21). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent acdd115 commit 8cbd863

1 file changed

Lines changed: 86 additions & 89 deletions

File tree

docs/guide/sortable-comparison.md

Lines changed: 86 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,91 +1,88 @@
11
# Sortable libraries comparison
22

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

Comments
 (0)