SortableJS Draggable Task Board — Trello-Style Kanban Snippet

SortableJS Draggable Task Board · Dashboards · Plain HTML, CSS & JS · Live preview

What's included

Features

Cross-column dragging via group
One shared group string is the entire mechanism enabling drops between separate lists.
Independent per-list instances
Each column is its own Sortable() call, not a single instance spanning the board.
Animated reflow
animation: 150 smoothly slides neighboring cards as a drag lands among them.
Three distinct drag-state classes
ghostClass, chosenClass, and dragClass style separate moments of a drag independently.
Live count badges
Column counts re-read the DOM on every sort rather than tracking a separate variable.
No re-render on drop
SortableJS moves the real DOM node, so a dropped card keeps its content and listeners intact.
Touch and mouse support
SortableJS handles both pointer types without separate code paths.
Responsive 3-to-1 column layout
The board collapses to a single stacked column on narrow viewports.

About this UI Snippet

SortableJS Draggable Task Board — What group Actually Unlocks

Screenshot of the SortableJS Draggable Task Board snippet rendered live

A single new Sortable(list, {...}) call makes one list's items reorderable within themselves — that part is the library's default behavior with zero extra configuration. This task board needs more than that: a card started in "To Do" has to be droppable into "In Progress" or "Done." That specific capability, drag *between* separate DOM containers, comes from exactly one option: group.

Why a shared group string, not a shared instance

js document.querySelectorAll('.stb-list').forEach(function (list) { new Sortable(list, { group: 'tasks', ... }); });

Each column gets its own Sortable instance — there's no single object managing all three lists together. What ties them into one drag surface is that every instance is configured with the identical string 'tasks' for group. SortableJS checks this value at drag time: when a card is picked up from one list, it looks at every *other* Sortable-managed list on the page and asks "does your group match mine?" Only lists with a matching group become valid drop targets; a fourth list configured with group: 'archive' would visually sit right next to these columns but refuse every drop from them. This is what makes group a genuinely different mechanism from just calling Sortable on a parent wrapping all three columns — SortableJS is explicitly designed around independent instances that opt into cross-container dragging by name, not a single instance spanning multiple lists.

animation: 150 and the reflow

animation: 150 is what makes the *other* cards in a list slide smoothly out of the way as a dragged card passes over or lands among them, rather than snapping instantly to their new positions. It's measured in milliseconds and applies to every reflow SortableJS triggers — both within a list during reorder and across lists when a card arrives from elsewhere.

The class hooks: ghost, chosen, drag

Three separate classes cover three separate moments of a drag, and mixing them up is a common source of "why does my board look wrong while dragging" bugs: - `ghostClass` styles the placeholder left behind in the original position while dragging — this snippet dims it to 35% opacity so it reads as "the space this card used to occupy." - `chosenClass` styles the card the instant it's picked up, for the whole duration of the drag, including after it's dropped in its animation-in — this snippet gives it a glowing outline. - `dragClass` styles the actual element following the cursor/touch point during the drag itself.

onSort and keeping counts honest

SortableJS fires onSort on the list the drop landed in whenever its child order changes — from a drag *or* a drop arriving from elsewhere. This snippet uses it to re-run updateCounts(), which just re-reads each column's live .children.length rather than maintaining a separate count variable that could drift out of sync with the actual DOM. Because SortableJS moves the real DOM node on drop (it doesn't clone and destroy), the moved card carries all of its original content and listeners with it automatically — there's no re-render step needed after a drop.

Build with AI

Build, Understand, Optimize, and Extend It With AI

This snippet is a good one to pressure-test an AI's understanding of a single option's scope: paste it into an assistant like Claude and ask it to explain precisely what would happen if the "In Progress" column's Sortable instance were configured with group: 'other-tasks' while the remaining two kept group: 'tasks' — the correct answer is that cards could still move between To Do and Done, but neither could exchange cards with In Progress in either direction. Then ask it to distinguish the exact visual moment each of ghostClass, chosenClass, and dragClass applies to, since those three are easy to blur together without seeing them side by side. To extend it: ask for a version that persists the board's state to localStorage using onSort or onEnd, a WIP column limit that visually warns (or blocks drops) past a maximum card count, or a version that adds a "add task" input per column that appends a new draggable card without re-initializing Sortable.

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 3-column Trello-style task board using SortableJS (v1.15.2, from a CDN) in plain HTML, CSS, and JavaScript.

Requirements:
- Three columns (To Do, In Progress, Done), each rendered from a JS array of task objects ({ title, tag }) into card elements, with a live count badge per column header.
- Initialize a SEPARATE new Sortable(listElement, { ... }) instance for EACH column's list — not one instance wrapping all three — but give every instance the identical group: 'tasks' option. Add a comment explaining that group is the specific mechanism that allows a card dragged from one independently-managed list to be dropped into another, and that without a matching group value on every list, SortableJS would only allow reordering within each list, never across them.
- Configure animation: 150 for a smooth reflow of neighboring cards, plus three distinct classes: ghostClass (styles the placeholder left behind at the origin), chosenClass (styles the card for its full selected duration), and dragClass (styles the element actively following the pointer) — style all three differently enough in CSS that their distinct roles are visible.
- Use the onSort callback (fired on any list whose children change) to re-run a function that recalculates and displays each column's card count by reading list.children.length directly, rather than maintaining a separate counter variable.
- Style it as a dark, premium Kanban board with rounded card panels, colored column-status dots, and small tag pills per card, responsive down to a single stacked column on narrow viewports.

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="stb-stage">
  <div class="stb-head">
    <span class="stb-tag">SortableJS · shared group</span>
    <h2>Task Board</h2>
    <p>Drag any card within a column to reorder it, or drop it into another column — every list shares one SortableJS group.</p>
  </div>
  <div class="stb-board">
    <div class="stb-col">
      <div class="stb-col-head"><span class="stb-dot stb-dot-todo"></span>To Do<span class="stb-count" id="countTodo">0</span></div>
      <div class="stb-list" id="colTodo" data-col="todo"></div>
    </div>
    <div class="stb-col">
      <div class="stb-col-head"><span class="stb-dot stb-dot-progress"></span>In Progress<span class="stb-count" id="countProgress">0</span></div>
      <div class="stb-list" id="colProgress" data-col="progress"></div>
    </div>
    <div class="stb-col">
      <div class="stb-col-head"><span class="stb-dot stb-dot-done"></span>Done<span class="stb-count" id="countDone">0</span></div>
      <div class="stb-list" id="colDone" data-col="done"></div>
    </div>
  </div>
</div>

Step by step

How to Use

  1. 1
    Add the SortableJS CDN scriptOne script tag — no companion CSS is shipped by the library.
  2. 2
    Give every column list the same group valuegroup: "tasks" on all three Sortable instances is what enables cross-column drops.
  3. 3
    Create one Sortable instance per listEach column gets its own new Sortable() call, not one shared instance for the whole board.
  4. 4
    Set animation for a smooth reflowanimation: 150 makes other cards slide out of the way instead of snapping instantly.
  5. 5
    Style the three drag-state classesghostClass, chosenClass, and dragClass each cover a different moment of the drag.
  6. 6
    Use onSort to keep derived state in syncRe-read live DOM counts rather than maintaining a separate counter that can drift.

Real-world uses

Common Use Cases

Kanban and project boards
A Trello-style task board for internal tools or a product's own project management feature.
Pipeline and status tracking
Move leads, tickets, or applications between stages with a drag instead of a dropdown.
Teaching the group option
A focused example of the one option that turns isolated lists into a connected drag surface.
Admin dashboard widgets
A reorderable, categorized card layout for an internal admin panel.

Got questions?

Frequently Asked Questions

It tells SortableJS which lists are allowed to exchange dragged items with each other. Every list configured with the same group value becomes a valid drop target for cards dragged from any other list sharing that value; lists with a different or missing group refuse those drops.

SortableJS is designed around one instance per draggable container, with group being the mechanism that connects otherwise-independent instances for cross-container drags. There is no single-instance API for "one Sortable spanning three columns" — group is how the library solves that instead.

ghostClass styles the placeholder left in the original position during a drag. chosenClass styles the card itself for the whole time it is selected, including drop animation. dragClass styles specifically the element actively following the cursor or touch point. They cover three different visual moments and can be styled independently.

No. SortableJS moves the actual DOM node from one list to another on drop rather than destroying and recreating it, so anything attached to that element — event listeners, data attributes, content — survives the move automatically.

onSort fires whenever a list's children change due to a drag, whether that's a reorder or an arrival from another column. Re-reading list.children.length directly from the DOM inside that callback guarantees the displayed count always matches reality, instead of relying on a separate counter that could get out of sync if an update path is missed.

It sets, in milliseconds, how long SortableJS takes to animate other items sliding into their new positions as a drag moves through or lands in a list. Without it, neighboring cards would jump to new positions instantly with no transition.