iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more
A small Kanban board needs three things: workflow columns, task cards, and rules for moving a card between columns. You can build that core with plain HTML, CSS, and JavaScript using the browser’s native drag-and-drop events—without a framework. This walkthrough starts with sample tasks and append-to-column movement, then shows how to extend the board with reordering, persistence, and non-drag controls.
1. Set up the board structure
Give each workflow column its own task list. The example uses To Do, In Progress, and Done, with a few starter cards so you can test movement before adding form behavior.
<main class="board">
<section class="column" data-column="todo">
<h2>To Do</h2>
<ul class="task-list">
<li class="task" data-task-id="task-1" draggable="true">Sketch the layout</li>
<li class="task" data-task-id="task-2" draggable="true">Write the first draft</li>
</ul>
</section>
<section class="column" data-column="progress">
<h2>In Progress</h2>
<ul class="task-list"></ul>
</section>
<section class="column" data-column="done">
<h2>Done</h2>
<ul class="task-list"></ul>
</section>
</main>
The data-task-id value gives each task a stable identifier. Keep identifiers unique within the board rather than assigning the same global DOM id to multiple elements.
The Tool Desk
Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →2. Make the board readable at different widths
A flexible layout can show columns side by side on wider screens and stack them on smaller ones. The 600-pixel breakpoint below is an example, not a universal device threshold; adjust it to suit your content.
#1 Best Overall
* {
box-sizing: border-box;
}
.board {
display: flex;
align-items: flex-start;
gap: 1rem;
}
.column {
flex: 1 1 0;
min-width: 0;
padding: 1rem;
border-radius: 0.5rem;
background: #f1f3f5;
}
.task-list {
min-height: 5rem;
margin: 0;
padding: 0;
list-style: none;
}
.task {
margin: 0 0 0.75rem;
padding: 0.75rem;
border-radius: 0.35rem;
background: white;
cursor: grab;
}
.task:active {
cursor: grabbing;
}
@media (max-width: 600px) {
.board {
flex-direction: column;
}
.column {
width: 100%;
}
}
3. Move cards between columns
For arbitrary elements such as list items, set draggable="true". Native drag and drop uses several events: dragstart begins the operation, dragover may fire repeatedly as the pointer moves over a target, drop handles an accepted release, and dragend provides cleanup. A drop target must cancel dragover with preventDefault(); otherwise the browser does not treat it as a valid target. See the MDN HTML Drag and Drop API reference.
This implementation writes a task identifier to the drag data store when dragging starts and reads it only when a drop occurs. The payload is text, not a JavaScript object. It also keeps a temporary reference to the dragged element so the task can be moved without querying the document for a repeated identifier.
Rank #2
const board = document.querySelector(".board");
let draggedTask = null;
board.addEventListener("dragstart", (event) => {
const task = event.target.closest(".task");
if (!task || !board.contains(task)) return;
draggedTask = task;
event.dataTransfer.effectAllowed = "move";
event.dataTransfer.setData("text/plain", task.dataset.taskId);
});
board.addEventListener("dragover", (event) => {
const list = event.target.closest(".task-list");
if (!list || !board.contains(list)) return;
if (!draggedTask) return;
event.preventDefault();
event.dataTransfer.dropEffect = "move";
});
board.addEventListener("drop", (event) => {
const list = event.target.closest(".task-list");
if (!list || !board.contains(list)) return;
const taskId = event.dataTransfer.getData("text/plain");
if (!draggedTask || taskId !== draggedTask.dataset.taskId) return;
event.preventDefault();
list.append(draggedTask);
});
board.addEventListener("dragend", () => {
draggedTask = null;
});
Delegating listeners to the board lets newly added task elements use the same handlers. Filtering to a task list inside this board avoids accepting unrelated drops. The browser’s drag data store has timing constraints: set custom data in dragstart, then retrieve it in drop, rather than assuming it can be read during every event.
4. Choose whether cards append or reorder
Append to the end
list.append(draggedTask) is the simplest behavior: every drop places the card after the list’s existing cards. It is enough for cross-column movement, but does not let someone choose an insertion point within a column.
Insert at a chosen position
To support reordering, calculate the nearest task position as the pointer moves and show a placeholder there. On drop, insert the dragged task before the task at that position; if there is no following task, append it. Remove the placeholder after dropping or when the pointer truly leaves the column.
Be careful with dragleave: it can fire when the pointer moves from a column into one of its child elements. Before removing a placeholder, check whether event.relatedTarget is still inside the column. Otherwise, the preview can disappear while the pointer is still over the same column. MDN’s drag-and-drop documentation describes the relevant events and target behavior.
Rank #4
5. Keep application state consistent
Moving a DOM element is enough for a minimal visual demo. If you add persistence, editing, or more complex ordering, keep structured task data as the source of truth—for example, task identifiers, titles, column keys, and positions—and render the lists from that state. Update the task’s column and order when it moves, then update the interface. This avoids relying on the current HTML markup as the only record of the board.
Do these 3 things before closing this tab:
1Repair Windows errors before they cause bigger problems2Fix the driver behind crashes, sound loss and screen glitches3Clear out junk files and repair common Windows errors6. Add refresh persistence only if you need it
For a single-browser project, localStorage can hold a serialized representation of board state and restore it when the page starts. That is browser-local persistence, not shared or server-backed collaboration. The storage format, handling of older saved data, and recovery behavior are design choices your application needs to define; see MDN’s localStorage reference for the API.
Best Value
- Save: after a state change, serialize the board data and write it to a named storage key.
- Load: at startup, read the key, parse the value, and render the board from the resulting data.
- Recover: handle missing or invalid saved data by starting with the sample board rather than leaving the page unusable.
7. Treat task creation and editing as extensions
The drag-and-drop core does not require a task form. Add creation, editing, and deletion as separate features: collect a title, add a task with a unique identifier to the state, update a task’s title when edited, and remove it from state when deleted. Then render the updated board. This keeps the first lesson focused on moving cards instead of mixing interaction concerns.
8. Provide an alternative to dragging
Dragging is not the only way to change a task’s status. Add explicit controls, such as a button or select menu for choosing a destination column, so people can move tasks without a pointer-based drag gesture. If ordering matters, offer controls to move a task up or down within its list. These are implementation recommendations, not a complete accessibility interaction standard.
There is also a text-selection trade-off: MDN notes that users may need to hold Alt or use the keyboard to select text inside a draggable element. Avoid making nested links or controls behave like accidental drag handles, and consider limiting the drag gesture to a dedicated handle if cards contain interactive content. See the MDN HTML Drag and Drop API reference.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

