Build a Simple To-Do App in Vanilla JavaScript

Read time: 2 minutes
Tarun Ranka
Tarun Ranka

In this post, we’ll walk through building a clean, filterable To-Do list app using just HTML, CSS, and JavaScript — no libraries or frameworks required.

You’ll learn:

  • How to manage state with JavaScript arrays
  • Handle form submissions
  • Update the DOM dynamically
  • Filter tasks by active/completed status

Let’s dive in 👇

🧱 The HTML Setup

We assume you’ve already created a basic HTML structure like:

  • <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

    Let’s break down the JS logic step by step.

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

    We grab references to the input, list, form, and filter buttons using getElementById.

    2. 📦 Storing Todos

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

    We initialize an empty array to store our to-dos and a filter to track which view is active (all, active, 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);
      });
    }

    We filter the todos based on the selected filter.

    Then we create <li> items for each visible to-do, including:

    • A checkbox to toggle done state
    • A delete button

    Re-render the list on any change.

    4. ➕ Adding a New Todo

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

    On form submit:

    • Prevent page refresh
    • Add the new todo to the todos array
    • Re-render the list

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

    This function:

    • Hooks up the form submit
    • Adds click listeners to each filter button
      (e.g. sets filter to "active" if todo-active-filter is clicked)

    📂 View the Source Code on GitHub

    Want to explore or fork the code?

    🔗 Browse the full project on GitHub →

    🧪 Final Thoughts

    This simple app demonstrates how much you can do with pure JavaScript:

    • No frameworks
    • No build tools
    • Just clean DOM manipulation

    ✅ Bonus Challenge: Try adding localStorage so your to-dos persist after a page refresh!