Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

* {
  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.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

6. 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.

  1. Save: after a state change, serialize the board data and write it to a named storage key.
  2. Load: at startup, read the key, parse the value, and render the board from the resulting data.
  3. Recover: handle missing or invalid saved data by starting with the sample board rather than leaving the page unusable.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.