Use this when you are teaching a backlog of work that moves through stages — columns of cards you can advance one step at a time, like a bug list going from spotted to shipped.
This is a copyable exemplar. Lift the board section below into a real lesson built from assets/lesson-template.html — keep the design tokens and the Simple → Technical pattern intact.
A team finds bugs faster than it can fix them. To stay sane, they pin every bug to a wall split into three columns — Triage (just spotted), In progress (someone is on it), and Done (shipped). A bug always lives in exactly one column, and work flows left to right.
Each card below carries the same things a real issue tracker shows: a short title, a colored label for the area of the app it touches, and a priority dot. Press a card's arrow to slide it to the next column — or grab a card and drag it anywhere. The counts at the top stay honest the whole time.
Think of it like… sticky notes on a kitchen wall. A note never sits in two places; you peel it off "to-do" and stick it under "doing". Nothing is lost — every note is in one column, and the wall always tells you how much is left.
The board is a tiny state machine. Every bug is one object holding a col field that can only be triage, progress, or done. The columns on screen are not the source of truth — the array of bug objects is. Moving a card just changes that one field and re-paints; both the arrow button and a drag-and-drop drop land in the same moveTo() function, so the two interactions can never disagree.
Because the UI is rebuilt from state on every change, the counts can never drift: render() reads the array, the badges count it, and a card physically can't appear in two columns. This is the same shape as a real kanban — Jira, Linear, Trello — minus the network. No framework, no library: one array, one render function, native HTML drag events.
function render() { cols.forEach(c => lists[c].innerHTML = ''); // clear the 3 columns bugs.forEach(bug => lists[bug.col].append(cardEl(bug))); cols.forEach(c => badge[c].textContent = bugs.filter(b => b.col === c).length); }moveTo — the only place a bug's column changes
function moveTo(id, col) { const bug = bugs.find(b => b.id === id); if (!bug || bug.col === col) return; bug.col = col; // single source of truth render(); // repaint from state }
A triage board makes an abstract idea — work has stages, and items move between them — something a reader can feel by clicking. The columns give the eye an instant read of "how much is where", and the arrow keeps the interaction obvious for anyone who has never dragged anything. Reach for it whenever you are teaching pipelines, review queues, ticket lifecycles, or any "this goes from A to B to C" workflow.