#javascript March 30, 2025 2 min

Build a Simple To-Do App in Vanilla JavaScript

Construct a clean, filterable task management app using only HTML, CSS, and JavaScript — no frameworks needed.

This tutorial walks through constructing a clean, filterable task management application using only HTML, CSS, and JavaScript — without relying on external libraries or frameworks.

Key Learning Objectives

  • Manage application state with JavaScript arrays
  • Handle form submission events
  • Dynamically manipulate the DOM
  • Implement filtering functionality for task status

The HTML Setup

Begin with this basic structure:

<form id="todo-form">
  <input type="text" id="todo-input" placeholder="What needs to be done?" />
</form>

<ul id="todo-list"></ul>

<div>
  <button id="todo-all-filter">All</button>
  <button id="todo-active-filter">Active</button>
  <button id="todo-completed-filter">Completed</button>
</div>

JavaScript Explained

1. Targeting DOM Elements

let todoInput = document.getElementById("todo-input");
let todoList = document.getElementById("todo-list");
let todoForm = document.getElementById("todo-form");

let todoAllFilter = document.getElementById("todo-all-filter");
let todoActiveFilter = document.getElementById("todo-active-filter");
let todoCompletedFilter = document.getElementById("todo-completed-filter");

Use getElementById() to reference the input field, list container, form, and filter buttons.

2. Storing Todos

let todos = [];
let filter = "all";

Initialize an empty array for task storage and a filter variable tracking the active view mode (all, active, or completed).

3. Rendering the To-Do List

function renderTodos() {
  todoList.innerHTML = "";
  let filteredTodos = todos.filter((todo) => {
    if (filter === "all") {
      return true;
    } else if (filter === "active") {
      return !todo.done;
    } else {
      return todo.done;
    }
  });

  filteredTodos.forEach((todo) => {
    let todoElement = document.createElement("li");

    let checkbox = document.createElement("input");
    checkbox.type = "checkbox";
    checkbox.checked = todo.done;
    checkbox.addEventListener("change", () => {
      todo.done = checkbox.checked;
      renderTodos();
    });

    let deleteButton = document.createElement("button");
    deleteButton.innerText = "Delete";
    deleteButton.addEventListener("click", () => {
      todos = todos.filter((t) => t !== todo);
      renderTodos();
    });

    todoElement.appendChild(checkbox);
    todoElement.appendChild(document.createTextNode(todo.value));
    todoElement.appendChild(deleteButton);

    todoList.appendChild(todoElement);
  });
}

Filter tasks based on the selected view, then create list item elements containing a checkbox for completion toggling and a delete button for removal.

4. Adding a New Todo

function addTodo(e) {
  e.preventDefault();
  let value = todoInput.value;
  todos.push({ value, done: false });
  renderTodos();
}

On form submission, prevent the default page reload, extract the input value, append a new task object to the collection, and refresh the display.

5. Initialization & Event Binding

function init() {
  todoForm.addEventListener("submit", addTodo);
  [todoAllFilter, todoActiveFilter, todoCompletedFilter].forEach((element) => {
    element.addEventListener("click", () => {
      filter = element.id.split("-")[1];
      renderTodos();
    });
  });
}
init();

Attach event listeners to the form and filter buttons. When a filter button is clicked, extract the filter name from its ID and update the display.

View the Source Code on GitHub

Full project available: Browse the complete project on GitHub

Final Thoughts

This demonstration showcases what pure JavaScript can accomplish:

  • No frameworks required
  • No build tools needed
  • Clean, straightforward DOM manipulation

Bonus Challenge: Implement localStorage to persist tasks across page refreshes.