SortableJS Nested Tree with Keyboard — Free JS Snippet

SortableJS Nested Sortable Tree with Keyboard Controls · Dashboards · Plain HTML, CSS & JS · Live preview

CategoryDashboards

What's included

Features

Every list, including empty child lists, is its own Sortable in a shared group
swapThreshold and invertSwap to stop nested drops jittering
fallbackOnBody so parent overflow cannot clip the dragged row
Drag handle so text selection and clicks do not start a drag
Keyboard-operable move up, move down, indent and outdent buttons
Focus restored to the same button after each move
Live recursive JSON serialisation read from the DOM
Titles inserted with textContent to avoid HTML injection

About this UI Snippet

SortableJS Nested Sortable Tree — HTML, CSS & JavaScript

Screenshot of the SortableJS Nested Sortable Tree with Keyboard Controls snippet rendered live

Nested lists — course outlines, navigation menus, category trees, task hierarchies — need drag and drop that understands depth. Dragging an item above or below its siblings is the easy case. The hard cases are dragging into a parent that has no children yet, dragging out to a shallower level, and doing both without the list flickering as the pointer crosses boundaries. SortableJS supports nesting by treating every list as its own Sortable instance, and this snippet shows the configuration that makes it behave.

The structural rule is that each ul, including the empty child list of every item, is a separate Sortable created with the same group name. Because every item always contains a ul — even when it is empty — there is always a drop target to move something into, and a small min-height with a dashed left border keeps empty lists visible and hittable. makeSortable is called for each list as it is built, and an item created later automatically gets its own list. Items are arranged with a drag handle, so clicking the text to select a row does not start a drag.

Two options are what make nested drops reliable. swapThreshold sets how far into a target the pointer must travel before the items swap; the default is very sensitive, and in nested lists the pointer is often over two candidate targets at once, so a value like 0.65 stops items jumping back and forth. invertSwap reverses that behaviour when dragging over the far side of an element, which improves dragging past the last item into a parent's next slot. fallbackOnBody attaches the dragged element to the body, so an ancestor with overflow or a transform cannot clip it — a classic reason a nested drag seems to vanish.

Drag and drop is not enough for accessibility: it cannot be operated with a keyboard, and a tree with only that one path excludes people. Each row therefore has move up, move down, indent and outdent buttons, implemented as plain DOM operations — indent appends the row to the previous sibling's child list, outdent inserts it after its parent — and focus is restored to the same button afterwards so repeated presses keep working. The structure panel serialises the tree recursively from the DOM after every change, which shows the practical output and confirms that the DOM is the source of truth. Titles are written with textContent, never innerHTML.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Ask an AI assistant like Claude to limit nesting depth, add collapse and expand toggles for branches, or persist the tree and sync it with a server.

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 nested sortable outline with SortableJS 1.15 loaded from a CDN.

Requirements:
- Render items as li elements each containing a row (drag grip, title, buttons) and an always-present child ul; create a Sortable for every ul with group 'outline', handle '.ns-grip', animation 150, fallbackOnBody: true, swapThreshold: 0.65 and invertSwap: true.
- Give empty child lists a min-height and dashed border so they can receive drops.
- Add up, down, indent and outdent buttons implemented with DOM operations (indent appends to the previous sibling's child list, outdent inserts after the parent) and restore focus to the same button afterwards.
- Serialise the outline recursively into JSON after every change and show it with an item count; use textContent for titles.

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="ns-app">
  <div class="ns-panel">
    <div class="ns-head"><h3>Course outline</h3><button type="button" id="nsAdd">+ Add section</button></div>
    <ul class="ns-list" id="nsRoot" role="tree" aria-label="Course outline"></ul>
    <p class="ns-help">Drag the grip to reorder or nest. Keyboard: select a row, then use the buttons &mdash; <kbd>&#8593;</kbd><kbd>&#8595;</kbd> move, <kbd>&#8677;</kbd> indent, <kbd>&#8676;</kbd> outdent.</p>
  </div>
  <div class="ns-panel ns-out">
    <div class="ns-head"><h3>Structure</h3><span id="nsCount">0 items</span></div>
    <pre id="nsJson" aria-live="polite"></pre>
  </div>
</div>

Step by step

How to Use

  1. 1
    Reorder siblingsDrag a row by its grip to move it above or below another row at the same level.
  2. 2
    Nest an itemDrag a row into the dashed area of another row to make it a child, even if that row has no children yet.
  3. 3
    Move outDrag a nested row to the left, out of its parent, to make it a top-level item again.
  4. 4
    Use the buttonsClick a row, then use the up, down, indent and outdent buttons to restructure without dragging.
  5. 5
    Read the structureThe JSON panel shows the outline as nested title and children objects, updated after every change.

Real-world uses

Common Use Cases

Course and documentation outlines
Let authors structure content by dragging. For a flat multi-column workflow see the kanban board.
Menu and navigation builders
Compose multi-level menus in a CMS.
ADMIN
Category and taxonomy editors
Reorganise product categories with visible hierarchy.
Learning nested Sortable setups
A practical example of the options nested lists need to work reliably.

Got questions?

Frequently Asked Questions

Create a Sortable on every list, including nested and empty ones, with the same group name. Use fallbackOnBody and a swapThreshold around 0.65.

An empty list has no height. Give nested lists a min-height (and always render the ul) so there is a drop target.

It sets how far into a target the dragged item must be before they swap. Higher values reduce jitter when several targets overlap.

A parent with overflow or a transform can clip it. Set fallbackOnBody: true so the drag clone is attached to the body.

Provide buttons or keyboard shortcuts that perform the same moves, and keep focus on the control after the DOM changes.

Walk the DOM recursively from the root list, reading each item's title and its child list, as toTree() does.

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.