SortableJS Kanban with WIP Limit — Free JS Snippet

SortableJS Multi-List Kanban with Persistence and WIP Limit · Dashboards · Plain HTML, CSS & JS · Live preview

CategoryDashboards

What's included

Features

Three lists sharing one group so cards move between them
group.put() function enforcing a work-in-progress limit
onMove message explaining why a drop is refused
The DOM read back after every drag as the single source of truth
localStorage persistence guarded for sandboxed frames
Touch delay so scrolling still works on phones
Ghost, chosen and drag classes for clear visual feedback
Add-task form with escaped titles and live column counts

About this UI Snippet

SortableJS Multi-List Kanban — HTML, CSS & JavaScript

Screenshot of the SortableJS Multi-List Kanban with Persistence and WIP Limit snippet rendered live

A kanban board is the canonical drag-and-drop interface: cards move between columns that represent stages. SortableJS makes the core of it almost trivial — create one Sortable per list and give them the same group name — and this snippet builds on that core with the three things that turn a demo into something usable: a rule that limits what a column accepts, saved state, and touch behaviour that does not break scrolling.

Lists that share a group name can exchange items. group can also be an object, and its put property is a function that decides whether a given list accepts a card being dragged over it. Here the Doing column refuses drops once it holds three cards — a work-in-progress limit, the mechanism that gives kanban its discipline. Because put runs while the card hovers, the list simply does not open a gap for it, which communicates the refusal without a modal. The onMove callback adds a message explaining why, and the column heading turns red when full.

Persistence introduces the most important architectural idea in the snippet: after a drag, the DOM is the source of truth. There is no separate model being updated during the drag. When onEnd fires, read() walks the three lists in their new order and rebuilds the state from the elements themselves, which is then written out. That keeps the model and the view impossible to disagree. The save is guarded because the preview here runs in a sandboxed frame with an opaque origin, where merely touching localStorage throws a SecurityError. The load and persist functions catch that and fall back to keeping the board in memory, with a status line saying which is happening — the same defensive pattern any embeddable widget needs.

On touch screens, an instant drag would make every scroll gesture pick up a card. Setting delay to 120 milliseconds with delayOnTouchOnly means a quick swipe scrolls the page while a short press-and-hold lifts a card, and mouse users are unaffected. The ghost, chosen and drag classes style the placeholder, the picked-up card and the card in flight, and the add-task form inserts a card at the top of To do and saves. Cards are inserted with text escaped, since task titles are user input.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Ask an AI assistant like Claude to sync the board to a backend with optimistic updates, add card details in a modal, or add per-column colour and swimlanes.

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 kanban board with SortableJS 1.15 loaded from a CDN.

Requirements:
- Create three lists (To do, Doing, Done) with Sortable.create sharing group 'kanban'; make the Doing list refuse drops through group.put() once it holds three cards.
- Use animation 160, ghostClass, chosenClass, dragClass, and delay 120 with delayOnTouchOnly.
- In onMove show a message when Doing is full; in onEnd read the state back from the DOM, update the column counts and persist it to localStorage inside try/catch, falling back to memory with a visible status when storage is blocked.
- Add a form that inserts a new card (escaped text) at the top of To do, and a Reset button restoring the default board.

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="kb-app">
  <div class="kb-top">
    <form class="kb-add" id="kbForm">
      <input id="kbNew" type="text" placeholder="Add a task to To do..." maxlength="60" aria-label="New task title">
      <button type="submit">Add</button>
    </form>
    <div class="kb-tools">
      <span class="kb-save" id="kbSave" role="status" aria-live="polite">-</span>
      <button type="button" id="kbReset">Reset board</button>
    </div>
  </div>
  <div class="kb-board">
    <section class="kb-col" data-col="todo"><h3>To do <b id="kbC-todo">0</b></h3><ul class="kb-list" id="kbL-todo"></ul></section>
    <section class="kb-col" data-col="doing"><h3>Doing <b id="kbC-doing">0</b><em id="kbLim">max 3</em></h3><ul class="kb-list" id="kbL-doing"></ul></section>
    <section class="kb-col" data-col="done"><h3>Done <b id="kbC-done">0</b></h3><ul class="kb-list" id="kbL-done"></ul></section>
  </div>
  <p class="kb-note" id="kbNote" aria-live="polite">Drag cards between columns. Doing holds three at most.</p>
</div>

Step by step

How to Use

  1. 1
    Drag a cardDrag a card from To do into Doing. Counts update and the board saves.
  2. 2
    Hit the WIP limitFill Doing with three cards, then drag a fourth over it. The column refuses and a message explains why.
  3. 3
    Reorder within a columnDrag a card up or down within its own column to change its priority.
  4. 4
    Add a taskType a title and press Add. The card appears at the top of To do.
  5. 5
    Reload and resetReload to see the saved board restored (where storage is available), or press Reset board.

Real-world uses

Common Use Cases

Project and sprint boards
Track work through stages with limits. For nested hierarchies see the nested sortable tree.
ADMIN
Support ticket triage
Move tickets through New, In progress and Resolved.
SHOP
Order and fulfilment pipelines
Drag orders between packing, shipped and delivered.
Learning shared groups and put rules
A compact example of group objects, onMove and DOM-as-model.

Got questions?

Frequently Asked Questions

Create a Sortable on each list and give them the same group name, for example group: "kanban".

Use group: { name, put: function (to) { return to.el.children.length < LIMIT; } } so the list refuses drops when full.

In onEnd, read the lists back from the DOM (or use toArray()) and store the result. The DOM is already in the new order.

Sandboxed iframes without allow-same-origin have an opaque origin, and storage access throws. Wrap it in try/catch and fall back to memory.

Set delay (for example 120) with delayOnTouchOnly: true so a quick swipe scrolls and a press-and-hold starts a drag.

Use ghostClass for the placeholder, chosenClass for the picked-up element and dragClass for the element being dragged.

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.