It’s Monday, baby! Let’s get started with a simple Kanban board. We’ll be using Tailwind CSS and JavaScript to create a Kanban board with three columns: To Do, In Progress, and Done.
A Kanban board is a visual tool used to manage tasks in a workflow. It organizes tasks into columns, representing different stages of the work process. For example, in a basic Kanban board, the columns might be To Do, In Progress, and Done.
Each task is represented by a “card” that moves through the workflow as work progresses, making it easy to visualize the status of each task and the overall project.
Kanban boards are used in many areas to streamline task management, including:
The markup for our Kanban board will consist of three columns: To Do, In Progress, and Done. Each column will contain a list of tasks, which will be represented by cards. The cards will have a title, description, and status and will move through the workflow as work progresses.
Id’s
id="kanban-board"
: This is the main container for the Kanban board. It will hold all the columns. We’ll use this id to select the container element in JavaScript.<div id="kanban-board">
<!-- Columns -->
</div>
The to-do column will contain a list of tasks, which will be represented by cards.
Id’s
id="todo"
: This is the container for the to-do column. It will hold all the tasks. We’ll use this id to select the container element in JavaScript.The form will allow users to add new tasks to the to-do column. It will have an input field for entering the task title and a submit button to add the task. Data attributes
data-column="todo"
: This is the data attribute that identifies the column the form belongs to.<div>
<h2>To Do</h2>
<div id="todo"></div>
<form data-column="todo">
<input type="text" placeholder="New task..." />
<button type="submit">Add Task</button>
</form>
</div>
The in-progress column will contain a list of tasks, which will be represented by cards.
Id’s
id="inProgress"
: This is the container for the in-progress column.The form will allow users to add new tasks to the in-progress column. Data attributes
data-column="inProgress"
: This is the data attribute that identifies the column the form belongs to.<div>
<h2>In Progress</h2>
<div id="inProgress"></div>
<form data-column="inProgress">
<input type="text" placeholder="New task..." />
<button type="submit">Add Task</button>
</form>
</div>
The done column will contain a list of tasks, which will be represented by cards. The cards will have a title, description, and status and will move through the workflow as work progresses.
Id’s
id="done"
: This is the container for the done column.The form will allow users to add new tasks to the done column. Data attributes
data-column="done"
: This is the data attribute that identifies the column the form belongs to.<div>
<h2>Done</h2>
<div id="done"></div>
<form data-column="done">
<input type="text" placeholder="New task..." />
<button type="submit">Add Task</button>
</form>
</div>
Classes are removed for brevity and clarity. Grab the full code from the button above
<div id="kanban-board">
<div>
<h2>To Do</h2>
<div id="todo"></div>
<form data-column="todo">
<input type="text" placeholder="New task..." />
<button type="submit">Add Task</button>
</form>
</div>
<div>
<h2>In Progress</h2>
<div id="inProgress"></div>
<form data-column="inProgress">
<input type="text" placeholder="New task..." />
<button type="submit">Add Task</button>
</form>
</div>
<div>
<h2>Done</h2>
<div id="done"></div>
<form data-column="done">
<input type="text" placeholder="New task..." />
<button type="submit">Add Task</button>
</form>
</div>
</div>
We’ll use the columns
array to store the column names. We’ll also use this array to loop through the columns and render the tasks.
const columns = ["todo", "inProgress", "done"];
let tasks = {
todo: [],
inProgress: [],
done: [],
};
We’ll use the localStorage
object to load tasks from localStorage. If tasks exist, we’ll parse them and store them in the tasks
object. We’ll also use this object to save tasks to localStorage when the user adds a new task.
const savedTasks = localStorage.getItem("kanbanTasks");
if (savedTasks) {
tasks = JSON.parse(savedTasks);
}
We’ll use the localStorage
object to save tasks to localStorage when the user adds a new task. We’ll also use this object to load tasks from localStorage.
function saveTasks() {
localStorage.setItem("kanbanTasks", JSON.stringify(tasks));
}
We’ll use the renderTasks
function to render the tasks in each column. This function will loop through each column and render the tasks in that column. We’ll also use this function to add event listeners to the task cards. When a user clicks on a task card, we’ll add the task to the corresponding column. When a user clicks on a move button, we’ll move the task from one column to another. When a user clicks on a delete button, we’ll delete the task from the column.
columns.forEach((column) => {
: This is a loop that iterates over each column in the columns
array.const columnElement = document.getElementById(column);
: This is a line of code that selects the corresponding column element in the DOM based on the column name.columnElement.innerHTML = "";
: This is a line of code that clears the content of the column element.tasks[column].forEach((task, index) => {
: This is a loop that iterates over each task in the corresponding column.const taskElement = document.createElement("div");
: This is a line of code that creates a new div
element to represent the task.taskElement.className = "bg-white p-2 rounded shadow flex justify-between items-center";
: This is a line of code that sets the class name of the task element.taskElement.innerHTML =
: This is a line of code that sets the inner HTML of the task element.<p class="flex-grow">${task}</p>
: This is a line of code that sets the task title.<div class="flex items-center space-x-1">
: This is a line of code that sets the task container.${column !== "todo" ?
.data-action=“move” data-from=”${column}” data-to=”${ columns[columns.indexOf(column) - 1] }” data-index=”${index}“ : ""}
: This is a line of code that adds a move button to the task container if the column is not the “To Do” column.${column !== "done" ?
data-action=“move” data-from=”${column}” data-to=”${ columns[columns.indexOf(column) + 1] }” data-index=”${index}“ : ""}
: This is a line of code that adds a move button to the task container if the column is not the “Done” column.<button data-action="delete" data-column="${column}" data-index="${index}">⤬</button>
: This is a line of code that adds a delete button to the task container.columnElement.appendChild(taskElement);
: This is a line of code that appends the task element to the column element.unction renderTasks() {
columns.forEach((column) => {
const columnElement = document.getElementById(column);
columnElement.innerHTML = "";
tasks[column].forEach((task, index) => {
const taskElement = document.createElement("div");
taskElement.className =
"bg-white p-2 rounded shadow flex justify-between items-center";
taskElement.innerHTML = `
<p class="flex-grow">${task}</p>
<div class="flex items-center space-x-1">
${
column !== "todo"
? `<button data-action="move" data-from="${column}" data-to="${
columns[columns.indexOf(column) - 1]
}" data-index="${index}" class="inline-flex items-center justify-center text-blue-500 hover:text-blue-600 p-1 rounded">
←
</button>`
: ""
}
${
column !== "done"
? `<button data-action="move" data-from="${column}" data-to="${
columns[columns.indexOf(column) + 1]
}" data-index="${index}" class="inline-flex items-center justify-center text-green-500 hover:text-green-600 p-1">
→
</button>`
: ""
}
<button data-action="delete" data-column="${column}" data-index="${index}" class="inline-flex items-center justify-center text-red-500 hover:text-red-600 p-1">
⤬
</button>
</div>
`;
columnElement.appendChild(taskElement);
});
});
}
We’ll use the addTask
function to add a new task to the corresponding column. This function will check if the task is empty and if so, it will do nothing.
if (task !== "") {
: This is a conditional statement that checks if the task is empty.tasks[column].push(task);
: This is a line of code that adds the task to the corresponding column.renderTasks();
: This is a line of code that renders the tasks in the corresponding column.saveTasks();
: This is a line of code that saves the tasks to localStorage.function addTask(column, task) {
if (task !== "") {
tasks[column].push(task);
renderTasks();
saveTasks();
}
}
We’ll use the moveTask
function to move a task from one column to another. This function will check if the task is empty and if so, it will do nothing.
const task = tasks[fromColumn].splice(taskIndex, 1)[ 0];
: This is a line of code that selects the task from the fromColumn
and removes it from the array.tasks[toColumn].push(task);
: This is a line of code that adds the task to the toColumn
array.renderTasks();
: This is a line of code that renders the tasks in the corresponding column.saveTasks();
: This is a line of code that saves the tasks to localStorage.function moveTask(fromColumn, toColumn, taskIndex) {
const task = tasks[fromColumn].splice(taskIndex, 1)[0];
tasks[toColumn].push(task);
renderTasks();
saveTasks();
}
We’ll use the deleteTask
function to delete a task from the corresponding column. This function will check if the task is empty and if so, it will do nothing.
tasks[column].splice(taskIndex, 1);
: This is a line of code that removes the task from the corresponding column.renderTasks();
: This is a line of code that renders the tasks in the corresponding column.saveTasks();
: This is a line of code that saves the tasks to localStorage.function deleteTask(column, taskIndex) {
tasks[column].splice(taskIndex, 1);
renderTasks();
saveTasks();
}
We will use event delegation to add event listeners to the move and delete buttons. This will allow us to handle the events for all buttons in the document without having to add event listeners to each button individually.
document.addEventListener("click", (event) => {
: This is a line of code that adds an event listener to the document.const button = event.target.closest("button[data-action]");
: This is a line of code that selects the closest button element with the data-action
attribute.if (button) {
: This is a conditional statement that checks if the button element exists.const { action, from, to, column, index } = button.dataset;
: This is a line of code that extracts the data attributes from the button element.if (action === "move") {
: This is a conditional statement that checks if the action is “move”.moveTask(from, to, parseInt(index));
: This is a line of code that calls the moveTask
function with the from
, to
, and index
parameters.} else if (action === "delete") {
: This is a conditional statement that checks if the action is “delete”.deleteTask(column, parseInt(index));
: This is a line of code that calls the deleteTask
function with the column
and index
parameters.renderTasks();
: This is a line of code that renders the tasks in the corresponding column.document.addEventListener("click", (event) => {
const button = event.target.closest("button[data-action]");
if (button) {
const {
action,
from,
to,
column,
index
} = button.dataset;
if (action === "move") {
moveTask(from, to, parseInt(index));
} else if (action === "delete") {
deleteTask(column, parseInt(index));
}
}
});
// The initial render
renderTasks();
const columns = ["todo", "inProgress", "done"];
let tasks = {
todo: [],
inProgress: [],
done: [],
};
// Load tasks from localStorage
const savedTasks = localStorage.getItem("kanbanTasks");
if (savedTasks) {
tasks = JSON.parse(savedTasks);
}
function saveTasks() {
localStorage.setItem("kanbanTasks", JSON.stringify(tasks));
}
function renderTasks() {
columns.forEach((column) => {
const columnElement = document.getElementById(column);
columnElement.innerHTML = "";
tasks[column].forEach((task, index) => {
const taskElement = document.createElement("div");
taskElement.className =
"bg-white p-2 rounded shadow flex justify-between items-center";
taskElement.innerHTML = `
<p class="flex-grow">${task}</p>
<div class="flex items-center space-x-1">
${
column !== "todo"
? `<button data-action="move" data-from="${column}" data-to="${
columns[columns.indexOf(column) - 1]
}" data-index="${index}" class="inline-flex items-center justify-center text-blue-500 hover:text-blue-600 p-1 rounded">
←
</button>`
: ""
}
${
column !== "done"
? `<button data-action="move" data-from="${column}" data-to="${
columns[columns.indexOf(column) + 1]
}" data-index="${index}" class="inline-flex items-center justify-center text-green-500 hover:text-green-600 p-1">
→
</button>`
: ""
}
<button data-action="delete" data-column="${column}" data-index="${index}" class="inline-flex items-center justify-center text-red-500 hover:text-red-600 p-1">
⤬
</button>
</div>
`;
columnElement.appendChild(taskElement);
});
});
}
function addTask(column, task) {
if (task !== "") {
tasks[column].push(task);
renderTasks();
saveTasks();
}
}
function moveTask(fromColumn, toColumn, taskIndex) {
const task = tasks[fromColumn].splice(taskIndex, 1)[0];
tasks[toColumn].push(task);
renderTasks();
saveTasks();
}
function deleteTask(column, taskIndex) {
tasks[column].splice(taskIndex, 1);
renderTasks();
saveTasks();
}
// Event delegation for form submissions
document.addEventListener("submit", (event) => {
if (event.target.tagName === "FORM") {
event.preventDefault();
const column = event.target.dataset.column;
const input = event.target.elements[0];
const task = input.value.trim();
addTask(column, task);
input.value = "";
}
});
// Event delegation for move and delete buttons
document.addEventListener("click", (event) => {
const button = event.target.closest("button[data-action]");
if (button) {
const {
action,
from,
to,
column,
index
} = button.dataset;
if (action === "move") {
moveTask(from, to, parseInt(index));
} else if (action === "delete") {
deleteTask(column, parseInt(index));
}
}
});
// Initial render
renderTasks();
This is a simple Kanban board example that demonstrates how to use Tailwind CSS and JavaScript to create a Kanban board with three columns: To Do, In Progress, and Done. It’s a great starting point for building more complex Kanban boards and task management tools.
Hope you enjoyed this tutorial and have a great day!
/Michael Andreuzza
Get lifetime access to every theme available today for $199 $139 and own
them forever. Plus, new themes, lifetime updates, use on unlimited
projects and enjoy lifetime support.
— No subscription required!