← Back to all tutorials

How to create a basic Kanban board with Tailwind CSS and JavaScript

js-kanban-board
Published and written on Sep 16 2024 by Michael Andreuzza

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.

What are Kanban boards?

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.

Use cases

Kanban boards are used in many areas to streamline task management, including:

  • Software Development: Tracking features, bugs, and tasks from backlog to deployment.
  • Project Management: Managing tasks, deadlines, and progress for various team members.
  • Personal Productivity: Organizing personal goals and tasks, keeping focus on what’s important. and many more.

The markup

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.

The wrapper

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

The to-do column will contain a list of tasks, which will be represented by cards.

The task container

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 task form

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

The in-progress column will contain a list of tasks, which will be represented by cards.

The task container

Id’s

  • id="inProgress": This is the container for the in-progress column.
The task form

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

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.

The task container

Id’s

  • id="done": This is the container for the done column.
The task form

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>

Now, let’s add some JavaScript to make it interactive!

The columns

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: [],
};

Load tasks from localStorage

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);
}

Save tasks to localStorage

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));
}

The renderTasks function

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);
      });
   });
}

The addTask function

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();
   }
}

The moveTask function

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();
}

The deleteTask function

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();
}

Event delegation for move and delete buttons

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();

The full script

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();

Conclusion

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

Did you like this tutorial? Please share it with your friends!

Reviews and opinions

Get lifetime access to every theme available today for $199 and own them forever. Plus, new themes, lifetime updates, use on unlimited projects and enjoy lifetime support.

No subscription required!

Lexington

Beautifully designed HTML, Astro.js and Tailwind themes! Save months of time and build your startup landing page in minutes.