SortableJS Sortable Table Rows with Undo — Free JS Snippet

SortableJS Sortable Table Rows with Live Ranking and Undo · Tables · Plain HTML, CSS & JS · Live preview

CategoryTables

What's included

Features

Sortable tbody with a grip-cell drag handle
Rank column renumbered in place, without re-rendering
Moved-row flash and an announcement of the old and new position
Undo history built from order snapshots (onStart and onEnd)
Bulk Sort by score routed through the same undo path
Alt+Arrow keyboard reordering with focusable rows
Cell-level ghost styling because row backgrounds are unreliable
Order modelled as a list of ids, so render and undo are simple

About this UI Snippet

SortableJS Sortable Table Rows — HTML, CSS & JavaScript

Screenshot of the SortableJS Sortable Table Rows with Live Ranking and Undo snippet rendered live

Reordering table rows by drag is one of the most requested interactions in admin tools — prioritised roadmaps, ranked shortlists, playlist and queue editors — and one of the ones that go wrong in interesting ways. Tables have quirks lists do not: a row is not a normal block element, so dragging it can collapse its column widths mid-drag, and the ranking column must stay in step with the visual order. SortableJS treats a tbody as a sortable list, and this snippet shows how to make the whole interaction feel finished.

Making a tbody sortable is one line: Sortable.create(tbody). The handle option restricts drag start to the grip cell, which matters more in tables than elsewhere because rows contain text people want to select. The dragged row's cells keep their widths in modern browsers, and the ghostClass styling has to target td elements rather than the tr — backgrounds on table rows are unreliable, so the CSS colours the cells. The demo uses the native HTML5 drag path; if column widths jump in a browser you support, forceFallback: true moves to Sortable's own drag implementation.

The ranking column is the important detail. After a drag, the numbers must reflect the new order. There are two ways: re-render the whole table, or update just the rank cells. Re-rendering is simple but interrupts the drop animation, so the snippet's renumber() walks the rows and rewrites only the rank text, and the moved row flashes briefly so the eye can find where it landed. A message reports the move — "Dark mode moved from #3 to #1" — which doubles as a live region for screen readers.

Every user-facing reorder tool deserves undo, and this one shows how cheap it can be when order is just a list of ids. onStart snapshots the order, onEnd pushes that snapshot to a history stack, and Undo pops it and re-renders through the same function that drew the table, so ranks and rows cannot disagree. The Sort by score button is a bulk reorder that goes through the same history path, so it too can be undone. Finally, drag-only reordering excludes keyboard users, so Alt with the up or down arrow moves the focused row, with the rows made focusable through tabindex.

Step by step

How to Use

  1. 1
    Drag a rowDrag a row by its grip. The rank numbers update and the moved row flashes.
  2. 2
    Read the messageA note below the table says which row moved and from which rank to which.
  3. 3
    UndoPress Undo to restore the previous order. Each move, and each sort, adds a history step.
  4. 4
    Sort by scorePress Sort by score to rank by impact over effort, then drag rows to override.
  5. 5
    Use the keyboardTab to a row and press Alt with the up or down arrow to move it.

Real-world uses

Common Use Cases

🗺️
Roadmap prioritisation
Rank initiatives by dragging rows by a grip cell, with the rank column renumbered in place and the moved row flashing briefly.
🎵
Playlist and queue editors
Reorder items that play or process in sequence, with an announcement of the old and new position for screen readers.
🏆
Shortlists and rankings
Rank candidates or options by drag, or sort by score, and use Alt plus Arrow keys to reorder without a mouse.
📊
Full-featured grids
Compare with the Tabulator sortable filterable grid when columns, filtering and large data sets matter more than manual ranking.
↩️
Undo history reference
Learn a minimal and reliable undo feature built from order snapshots, taken in the onStart and onEnd hooks of the drag.

Got questions?

Frequently Asked Questions

Yes. Call Sortable.create on the tbody. Use a handle cell so text remains selectable.

Table rows can lose column widths while dragged. Style the ghost through td elements, and try forceFallback: true if a browser misbehaves.

In onEnd, loop through the rows and rewrite each rank cell with its index plus one.

Snapshot the order in onStart, push it to a stack in onEnd, and on Undo pop it and re-render the table from that list of ids.

Make rows focusable and handle a shortcut such as Alt+ArrowUp/Down that moves the row and re-focuses it.

Map over tbody.children and read each row's data-id, or use sortable.toArray().

Yes. Use the JSX, Vue, Angular or Tailwind export buttons on this page to convert the markup and styles. The behaviour comes from SortableJS, so in a framework project install it with npm install sortablejs (or react-sortablejs / vuedraggable) instead of the CDN tag, create it in useEffect / onMounted / ngAfterViewInit, and sync your state from onEnd, and release it with destroy() when the component unmounts.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Ask an AI assistant like Claude to send the new order to an API, add a redo stack, or animate the rank numbers when they change.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a drag-to-rank table with SortableJS 1.15 loaded from a CDN.

Requirements:
- Render six roadmap rows in a tbody with a grip cell, a rank cell, initiative, team pill and impact/effort numbers; call Sortable.create on the tbody with handle '.grip' and a ghostClass styled on the td cells.
- In onEnd renumber the rank cells in place, flash the moved row and show a note like "X moved from #a to #b".
- Implement undo using order snapshots taken in onStart and pushed onto a history stack, re-rendering from an array of ids; add a Sort by score button (impact/effort) that also records history.
- Add Alt+ArrowUp/ArrowDown keyboard reordering with focusable rows.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Source Code

Requires
<div class="st-card">
  <div class="st-bar">
    <div><h3>Roadmap priorities</h3><p>Drag rows to rank them. Rank and score update as you go.</p></div>
    <div class="st-btns">
      <button type="button" id="stUndo" disabled>&#8630; Undo</button>
      <button type="button" id="stSort">Sort by score</button>
    </div>
  </div>
  <table class="st-table" aria-label="Roadmap priorities">
    <thead><tr><th class="c">#</th><th></th><th>Initiative</th><th>Team</th><th class="r">Impact</th><th class="r">Effort</th></tr></thead>
    <tbody id="stBody"></tbody>
  </table>
  <p class="st-note" id="stNote" aria-live="polite">Top of the list ships first.</p>
</div>