Skip to content

JavaScript Tutorial Practical Projects

Project 2: A To-Do List

Lesson 241 of 258 2 min read By Deepak Paswan Updated Oct 4, 2026

A to-do list is the classic project, because it uses nearly everything: arrays of objects, rendering, event delegation and (later) storage.

index.html
HTML
<style>
  .todo { font-family: system-ui, sans-serif; max-width: 320px; }
  form { display: flex; gap: 6px; }
  input[type="text"] { flex: 1; padding: 8px; font: inherit; }
  button { font: inherit; padding: 6px 12px; cursor: pointer; }
  ul { list-style: none; padding: 0; margin: 12px 0; }
  li { display: flex; align-items: center; gap: 8px; padding: 6px 0; border-bottom: 1px solid #eee; }
  li.done span { text-decoration: line-through; color: #888; }
  li span { flex: 1; }
  .count { color: #555; font-size: 14px; }
</style>

<div class="todo">
  <form id="form">
    <label for="task" class="sr" style="position:absolute;left:-9999px">New task</label>
    <input type="text" id="task" placeholder="What needs doing?" autocomplete="off">
    <button>Add</button>
  </form>
  <ul id="list"></ul>
  <p class="count" id="count" role="status"></p>
</div>

<script>
  let todos = [
    { id: 1, text: "Learn JavaScript", done: true },
    { id: 2, text: "Build a to-do list", done: false }
  ];
  let nextId = 3;

  const list = document.querySelector("#list");
  const countEl = document.querySelector("#count");

  function render() {
    list.replaceChildren(...todos.map(todo => {
      const li = document.createElement("li");
      li.dataset.id = todo.id;
      li.classList.toggle("done", todo.done);

      const box = document.createElement("input");
      box.type = "checkbox";
      box.checked = todo.done;
      box.setAttribute("aria-label", "Done: " + todo.text);

      const text = document.createElement("span");
      text.textContent = todo.text;

      const del = document.createElement("button");
      del.textContent = "Delete";
      del.dataset.action = "delete";

      li.append(box, text, del);
      return li;
    }));

    const left = todos.filter(t => !t.done).length;
    countEl.textContent = `${left} task${left === 1 ? "" : "s"} left`;
  }

  document.querySelector("#form").addEventListener("submit", event => {
    event.preventDefault();
    const input = document.querySelector("#task");
    const text = input.value.trim();
    if (!text) return;
    todos = [...todos, { id: nextId++, text, done: false }];
    input.value = "";
    render();
  });

  // one listener for the whole list (event delegation)
  list.addEventListener("click", event => {
    const li = event.target.closest("li");
    if (!li) return;
    const id = Number(li.dataset.id);

    if (event.target.dataset.action === "delete") {
      todos = todos.filter(t => t.id !== id);
      render();
    }
  });

  list.addEventListener("change", event => {
    const li = event.target.closest("li");
    const id = Number(li.dataset.id);
    todos = todos.map(t => (t.id === id ? { ...t, done: event.target.checked } : t));
    render();
  });

  render();
</script>

The ideas used

  • State is an array of objects (todos). The page is drawn from it by render().
  • Immutable updates: adding uses spread, deleting uses filter, toggling uses map (Spread, filter).
  • Event delegation: one click and one change listener on the list handle every row, including rows added later (Event Delegation).
  • textContent keeps user text safe (Safe Output).
  • data-id links each row to its task (Data Attributes).
  • Accessibility: a label for the input and each checkbox, a status message for the counter.

Next steps

  1. Save to localStorage: after every change, localStorage.setItem("todos", JSON.stringify(todos)), and load it at the start (JSON with localStorage).
  2. Add filters: All, Active, Done.
  3. Let users edit a task by double-clicking it.
  4. Add Clear completed.

Exercise: clear completed

Add a “Clear completed” button that removes every finished task.

Show a possible solution
index.html
HTML
<button id="clear">Clear completed</button>
script.js
JS
document.querySelector("#clear").addEventListener("click", () => {
  todos = todos.filter(t => !t.done);
  render();
});