31. JavaScript Practical Project: Task Tracker

What you will build

A task tracker: add tasks, tick them off, delete them, filter by status, and see a live count. It uses variables, arrays of objects, functions, the DOM, events, delegation, and safe text handling.

Before you start

  • Build it step by step in the Try it Yourself editor, or save the finished file as tracker.html on your computer.
  • Run the code after every step and check what happens.

The plan

  1. State: one array holds all the tasks. Each task is an object { id, text, done }.
  2. Render: one function draws the whole list from the state.
  3. Events: clicking changes the state, then calls render again.

The golden rule: change the data, then redraw. Never try to keep the page and the data in step by hand.

Step 1: The HTML

Step 1: HTML

<h2>Task tracker</h2>

<form id="form">
  <label for="task">New task</label>
  <input id="task" type="text" autocomplete="off">
  <button type="submit">Add</button>
</form>

<p id="message" role="status"></p>

<div id="filters">
  <button type="button" data-filter="all" aria-pressed="true">All</button>
  <button type="button" data-filter="open" aria-pressed="false">To do</button>
  <button type="button" data-filter="done" aria-pressed="false">Done</button>
</div>

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

Step 2: The state

Step 2: JavaScript

let tasks = [
  { id: 1, text: "Finish the JavaScript course", done: false },
  { id: 2, text: "Build a project", done: false }
];
let nextId = 3;
let filter = "all";

Step 3: The render function

It builds list items with createElement and textContent, so text typed by a visitor can never run as code.

Step 3: JavaScript

function visibleTasks() {
  if (filter === "open") { return tasks.filter((t) => !t.done); }
  if (filter === "done") { return tasks.filter((t) => t.done); }
  return tasks;
}

function render() {
  const list = document.getElementById("list");
  list.textContent = "";

  visibleTasks().forEach(function (task) {
    const li = document.createElement("li");
    li.dataset.id = task.id;

    const check = document.createElement("input");
    check.type = "checkbox";
    check.checked = task.done;
    check.id = "task-" + task.id;

    const label = document.createElement("label");
    label.htmlFor = check.id;
    label.textContent = task.text;

    const del = document.createElement("button");
    del.type = "button";
    del.textContent = "Delete";
    del.setAttribute("aria-label", "Delete task: " + task.text);

    li.append(check, " ", label, " ", del);
    list.appendChild(li);
  });

  const left = tasks.filter((t) => !t.done).length;
  document.getElementById("message").textContent =
    left + " of " + tasks.length + " tasks left";
}

Step 4: The events

Step 4: JavaScript

// Add a task
document.getElementById("form").addEventListener("submit", function (event) {
  event.preventDefault();
  const input = document.getElementById("task");
  const text = input.value.trim();
  if (text === "") { input.focus(); return; }

  tasks.push({ id: nextId++, text: text, done: false });
  input.value = "";
  input.focus();
  render();
});

// Tick or delete: one listener on the list (event delegation)
document.getElementById("list").addEventListener("click", function (event) {
  const li = event.target.closest("li");
  if (!li) { return; }
  const id = Number(li.dataset.id);

  if (event.target.matches("button")) {
    tasks = tasks.filter((t) => t.id !== id);
    render();
  } else if (event.target.matches("input[type=checkbox]")) {
    const task = tasks.find((t) => t.id === id);
    task.done = event.target.checked;
    render();
  }
});

// Filters
document.getElementById("filters").addEventListener("click", function (event) {
  const button = event.target.closest("button");
  if (!button) { return; }
  filter = button.dataset.filter;

  document.querySelectorAll("#filters button").forEach(function (b) {
    b.setAttribute("aria-pressed", b === button ? "true" : "false");
  });
  render();
});

render();

The complete file

Press Try it Yourself to run the finished tracker.

Example: complete page

<style>
  body { font-family: system-ui, sans-serif; max-width: 520px; margin: 0 auto; padding: 12px; color: #17345c; }
  form { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
  input[type="text"] { flex: 1 1 160px; padding: 8px; border: 1px solid #cfe6ff; border-radius: 6px; font: inherit; }
  button { padding: 8px 14px; border: 1px solid #0d6efd; background: #0d6efd; color: #fff; border-radius: 6px; font: inherit; cursor: pointer; }
  #filters { display: flex; gap: 6px; margin: 10px 0; }
  #filters button { background: #fff; color: #0d6efd; }
  #filters button[aria-pressed="true"] { background: #082b59; color: #fff; border-color: #082b59; }
  ul { list-style: none; padding: 0; }
  li { display: flex; align-items: center; gap: 8px; padding: 8px 0; border-bottom: 1px solid #cfe6ff; }
  li label { flex: 1; }
  li input:checked + label { text-decoration: line-through; color: #4f6b8f; }
  li button { background: #fff; color: #b02a37; border-color: #b02a37; padding: 4px 10px; }
  :focus-visible { outline: 3px solid #0d6efd; outline-offset: 2px; }
</style>

<h2>Task tracker</h2>

<form id="form">
  <label for="task">New task</label>
  <input id="task" type="text" autocomplete="off">
  <button type="submit">Add</button>
</form>

<p id="message" role="status"></p>

<div id="filters">
  <button type="button" data-filter="all" aria-pressed="true">All</button>
  <button type="button" data-filter="open" aria-pressed="false">To do</button>
  <button type="button" data-filter="done" aria-pressed="false">Done</button>
</div>

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

<script>
  let tasks = [
    { id: 1, text: "Finish the JavaScript course", done: false },
    { id: 2, text: "Build a project", done: false }
  ];
  let nextId = 3;
  let filter = "all";

  function visibleTasks() {
    if (filter === "open") { return tasks.filter((t) => !t.done); }
    if (filter === "done") { return tasks.filter((t) => t.done); }
    return tasks;
  }

  function render() {
    const list = document.getElementById("list");
    list.textContent = "";

    visibleTasks().forEach(function (task) {
      const li = document.createElement("li");
      li.dataset.id = task.id;

      const check = document.createElement("input");
      check.type = "checkbox";
      check.checked = task.done;
      check.id = "task-" + task.id;

      const label = document.createElement("label");
      label.htmlFor = check.id;
      label.textContent = task.text;

      const del = document.createElement("button");
      del.type = "button";
      del.textContent = "Delete";
      del.setAttribute("aria-label", "Delete task: " + task.text);

      li.append(check, " ", label, " ", del);
      list.appendChild(li);
    });

    const left = tasks.filter((t) => !t.done).length;
    document.getElementById("message").textContent =
      left + " of " + tasks.length + " tasks left";
  }

  document.getElementById("form").addEventListener("submit", function (event) {
    event.preventDefault();
    const input = document.getElementById("task");
    const text = input.value.trim();
    if (text === "") { input.focus(); return; }

    tasks.push({ id: nextId++, text: text, done: false });
    input.value = "";
    input.focus();
    render();
  });

  document.getElementById("list").addEventListener("click", function (event) {
    const li = event.target.closest("li");
    if (!li) { return; }
    const id = Number(li.dataset.id);

    if (event.target.matches("button")) {
      tasks = tasks.filter((t) => t.id !== id);
      render();
    } else if (event.target.matches("input[type=checkbox]")) {
      const task = tasks.find((t) => t.id === id);
      task.done = event.target.checked;
      render();
    }
  });

  document.getElementById("filters").addEventListener("click", function (event) {
    const button = event.target.closest("button");
    if (!button) { return; }
    filter = button.dataset.filter;

    document.querySelectorAll("#filters button").forEach(function (b) {
      b.setAttribute("aria-pressed", b === button ? "true" : "false");
    });
    render();
  });

  render();
</script>

How it works

  1. All data lives in tasks. The page is only a picture of it.
  2. render() clears the list and draws it again from the data and the current filter.
  3. Every action changes the data first, then calls render().
  4. One click listener on the list handles every checkbox and Delete button, using event.target. It also works for tasks added later.
  5. Task text goes in with textContent, so typing <b> shows the characters instead of running them.

Saving tasks on a real page

The editor blocks localStorage, but on your own website you can add saving with a few lines:

Syntax

// 1. Replace the starting tasks with saved ones
let tasks = JSON.parse(localStorage.getItem("tasks")) || [];
let nextId = tasks.reduce((max, t) => Math.max(max, t.id), 0) + 1;

// 2. At the end of render(), save them
localStorage.setItem("tasks", JSON.stringify(tasks));

Wrap both in try...catch, in case storage is unavailable. Remember that this data stays only in that one browser.

Challenges

  • Add a Clear completed button.
  • Let the visitor edit a task by double-clicking it.
  • Add a due date to each task and sort by it.
  • Add saving with localStorage and try...catch.
  • Split the code into modules: state.js, render.js, main.js.

Common mistakes

  • Changing the page without changing the data, so they get out of step.
  • Using innerHTML with the task text.
  • Forgetting preventDefault() on the form.
  • Forgetting to call render() after changing the data.
  • Duplicate task ids, which break the delete and tick logic.

Summary

  • You built a complete interactive app with state, rendering, and events.
  • The rule “change the data, then redraw” keeps apps reliable.
  • You used safe text handling, delegation, filters, and accessible controls.
Scroll to Top