SortableJS Handle and Locked Rows — Free JS Snippet

SortableJS Drag Handle with Pinned and Locked Rows · Dashboards · Plain HTML, CSS & JS · Live preview

CategoryDashboards

What's included

Features

Drag handle so the rest of each row stays interactive
filter for locked rows that cannot be picked up
preventOnFilter: false so buttons inside filtered rows still work
onMove guard so movable rows cannot displace a locked row
Pin toggle that rebuilds the row and restores focus
Fisher–Yates shuffle that preserves pinned positions
Live pinned/movable badge and aria-pressed state on pin buttons
Ghost styling and touch-action: none on the handle

About this UI Snippet

SortableJS Drag Handle with Locked Rows — HTML, CSS & JavaScript

Screenshot of the SortableJS Drag Handle with Pinned and Locked Rows snippet rendered live

Real lists are rarely all-or-nothing. A dashboard has widgets that users may rearrange and a few that must stay where they are; a checklist has mandatory items pinned to the top; a menu keeps "Home" first. SortableJS has the options for this, but they are easy to combine wrongly, and each one has a behaviour that is not obvious from its name. This snippet puts the three together — a handle, a filter and an onMove guard — and explains why all three are needed.

The handle option restricts drag start to elements matching a selector, here the grip icon. Without it, pressing anywhere on a row starts a drag, which fights with text selection, buttons inside the row and touch scrolling. A handle turns the rest of the row back into ordinary content. The filter option is the locking mechanism: elements matching .locked cannot be picked up. But filter has a side-effect that catches nearly everyone. By default Sortable calls preventDefault on any pointer event that lands on a filtered element, so the unpin button inside a locked row silently stops working. Setting preventOnFilter: false lets clicks through while the row is still undraggable.

The third piece is the one people forget. filter only stops a locked row from being dragged; it does nothing about another row being dragged over it. Drag a movable row onto a pinned one and Sortable will happily swap them, shifting the pinned row to a new position — precisely what pinning was meant to prevent. The onMove callback receives the row being hovered as evt.related, and returning false vetoes that move, so movable rows can only be placed where there is no lock.

Two supporting details make the demo behave. Toggling a lock rebuilds that single row so its class, aria-pressed state, label and icon cannot drift out of step, then restores focus to the button. And Shuffle unpinned randomises only the movable rows and places them back into the movable slots while pinned rows keep their exact indexes, a small algorithm that is worth reading: collect the movable rows, shuffle that array with Fisher–Yates, then walk the original order and substitute only the non-locked positions.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Ask an AI assistant like Claude to save the order to local storage, add per-widget resize options, or sync the pinned state with a user profile API.

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 reorderable widget list with SortableJS 1.15 loaded from a CDN.

Requirements:
- Render six widget rows with a grip handle, icon, name and a pin button; use Sortable.create with handle '.lk-grip', filter '.locked' and preventOnFilter: false.
- Add onMove returning false when evt.related has the locked class so movable rows cannot displace locked ones.
- The pin button toggles the lock by rebuilding that row, updating aria-pressed and aria-label, and restoring focus; show a "n pinned · m movable" badge.
- Add a Shuffle button that randomises only unpinned rows while keeping pinned rows at their positions, and a Reset button.

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="lk-card">
  <div class="lk-head">
    <div><h3>Dashboard widgets</h3><p>Pinned widgets stay put; everything else can be reordered.</p></div>
    <span class="lk-badge" id="lkBadge" role="status" aria-live="polite"></span>
  </div>
  <ul class="lk-list" id="lkList"></ul>
  <div class="lk-foot"><button type="button" id="lkShuffle">Shuffle unpinned</button><button type="button" id="lkReset">Reset order</button></div>
</div>

Step by step

How to Use

  1. 1
    Drag a widgetDrag any unpinned row by its grip to reorder it.
  2. 2
    Try to move a pinned rowTry to drag Revenue or Error rate. The grips are disabled and the row cannot be picked up.
  3. 3
    Drop onto a pinned rowDrag a movable row over a pinned one. The pinned row does not move and stays exactly where it is.
  4. 4
    Pin and unpinClick the lock button on any row. The unpin button works inside locked rows because preventOnFilter is off.
  5. 5
    ShufflePress Shuffle unpinned. Only the movable rows are rearranged; pinned rows keep their positions.

Real-world uses

Common Use Cases

Customisable dashboards
Let users arrange widgets while keeping mandatory ones fixed. For multi-column boards see the kanban snippet.
ADMIN
Menu and settings ordering
Reorder items while keeping required entries in place.
Checklists with mandatory steps
Pin required items to the top of a user-editable list.
Learning filter versus onMove
See why filter alone does not protect a row from being displaced.

Got questions?

Frequently Asked Questions

handle limits where a drag can start. filter names elements that must not be dragged at all.

Sortable calls preventDefault on filtered elements by default. Set preventOnFilter: false to let clicks through.

filter only stops it being dragged. Use onMove and return false when evt.related is a locked row.

Collect the movable rows, shuffle that array, then rebuild the list by substituting only the non-locked positions.

Use a real button with aria-pressed and an aria-label that changes, and restore focus after re-rendering the row.

Yes. Add touch-action: none to the handle so the browser does not scroll instead of dragging.

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.