JavaScript Tutorial

JavaScript Project: To-Do List

Add, complete, and delete tasks. Keep the list in an array and redraw the page.

What you will build

A to-do list. The visitor types a task, adds it, marks it complete, or deletes it. The visible list is always a redraw of one array in memory.

You do not splice DOM nodes by hand for every change. You change the array — push, toggle a flag, orfilter out an id — then render. The page stays in sync because it has one source of truth.

Open an example with Try it in JavaScript. That loads /javascript/try — a live page and a console.

Methods you will use

MethodJob on this page
arrayHolds each task as { id, text, done }
filterDeletes a task by keeping every other id
eventsAdd on click or Enter, toggle done, delete
mapFlips done on the matching task
createElementBuilds each row during render

Give every task an id. Positions change when you delete. An id stays stable, so the Done and Delete buttons always point at the right item.

Build in slices

A field, an add button, a status line, and an empty list. The script will fill the list.

Example

<label for="task">New task</label>
<input id="task" type="text">
<button id="add" type="button">Add</button>
<p id="status">0 tasks</p>
<ul id="list"></ul>

Keep the array and a function that turns it into list items. Empty text is not a task. After a successful add, clear the field.

Example

let tasks = [];
let nextId = 1;

function addTask(raw) {
  const text = raw.trim();
  if (!text) return;
  tasks.push({ id: nextId, text: text, done: false });
  nextId += 1;
}

function toggleTask(id) {
  tasks = tasks.map(function (task) {
    if (task.id === id) return { id: task.id, text: task.text, done: !task.done };
    return task;
  });
}

function deleteTask(id) {
  tasks = tasks.filter(function (task) {
    return task.id !== id;
  });
}

Render wipes the list and rebuilds it. Each row gets a Done button and a Delete button that close over that row's id.

Example

<p id="status"></p>
<ul id="list"></ul>
<script>
  let tasks = [
    { id: 1, text: "Read the lesson", done: true },
    { id: 2, text: "Build the list", done: false }
  ];
  const list = document.getElementById("list");
  const status = document.getElementById("status");

  function toggleTask(id) {
    tasks = tasks.map(function (task) {
      if (task.id === id) return { id: task.id, text: task.text, done: !task.done };
      return task;
    });
  }

  function deleteTask(id) {
    tasks = tasks.filter(function (task) { return task.id !== id; });
  }

  function render() {
    list.replaceChildren();
    tasks.forEach(function (task) {
      const item = document.createElement("li");
      const label = document.createElement("span");
      label.textContent = task.text;
      if (task.done) label.style.textDecoration = "line-through";

      const doneBtn = document.createElement("button");
      doneBtn.type = "button";
      doneBtn.textContent = task.done ? "Undo" : "Done";
      doneBtn.addEventListener("click", function () {
        toggleTask(task.id);
        render();
      });

      const deleteBtn = document.createElement("button");
      deleteBtn.type = "button";
      deleteBtn.textContent = "Delete";
      deleteBtn.addEventListener("click", function () {
        deleteTask(task.id);
        render();
      });

      item.append(label, doneBtn, deleteBtn);
      list.appendChild(item);
    });
    status.textContent = tasks.length + (tasks.length === 1 ? " task" : " tasks");
  }

  render();
</script>

Add three tasks, complete the middle one, delete the first. If the remaining labels are wrong, the id was the index. Test at /javascript/try with Try it in JavaScript.

Complete document

This file is the list you would keep. The array starts empty. Add, Done, Undo, and Delete all callrender. Remaining open tasks are counted in the status line.

Example

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>To-Do List</title>
  <style>
    body {
      margin: 0;
      min-height: 100vh;
      display: grid;
      place-items: center;
      background: #fefce8;
      font-family: "Segoe UI", system-ui, sans-serif;
      color: #1c1917;
    }
    .card {
      width: min(26rem, 92vw);
      background: #fffbeb;
      border: 1px solid #fde68a;
      padding: 1.4rem 1.5rem 1.5rem;
    }
    h1 { margin: 0 0 0.85rem; font-size: 1.4rem; }
    .row { display: flex; gap: 0.45rem; margin-bottom: 0.75rem; }
    label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
    input {
      flex: 1;
      padding: 0.45rem 0.55rem;
      border: 1px solid #d6d3d1;
      font: inherit;
    }
    button {
      padding: 0.4rem 0.65rem;
      border: 0;
      background: #854d0e;
      color: #fffbeb;
      font: inherit;
      font-weight: 700;
      cursor: pointer;
    }
    #status { margin: 0 0 0.6rem; color: #78716c; }
    ul { list-style: none; margin: 0; padding: 0; }
    li {
      display: flex;
      gap: 0.4rem;
      align-items: center;
      padding: 0.4rem 0;
      border-top: 1px solid #fde68a;
    }
    li span { flex: 1; }
    li button { background: #d6d3d1; color: #1c1917; font-weight: 600; }
    li button.delete { background: #fef3c7; }
  </style>
</head>
<body>
  <article class="card">
    <h1>To-Do List</h1>
    <div class="row">
      <label for="task">New task</label>
      <input id="task" type="text" placeholder="Add a task">
      <button id="add" type="button">Add</button>
    </div>
    <p id="status"></p>
    <ul id="list"></ul>
  </article>
  <script>
    let tasks = [];
    let nextId = 1;
    const field = document.getElementById("task");
    const list = document.getElementById("list");
    const status = document.getElementById("status");

    function addTask(raw) {
      const text = raw.trim();
      if (!text) return;
      tasks.push({ id: nextId, text: text, done: false });
      nextId += 1;
      field.value = "";
    }

    function toggleTask(id) {
      tasks = tasks.map(function (task) {
        if (task.id === id) {
          return { id: task.id, text: task.text, done: !task.done };
        }
        return task;
      });
    }

    function deleteTask(id) {
      tasks = tasks.filter(function (task) {
        return task.id !== id;
      });
    }

    function render() {
      list.replaceChildren();
      tasks.forEach(function (task) {
        const item = document.createElement("li");
        const label = document.createElement("span");
        label.textContent = task.text;
        if (task.done) label.style.textDecoration = "line-through";

        const doneBtn = document.createElement("button");
        doneBtn.type = "button";
        doneBtn.textContent = task.done ? "Undo" : "Done";
        doneBtn.addEventListener("click", function () {
          toggleTask(task.id);
          render();
        });

        const deleteBtn = document.createElement("button");
        deleteBtn.type = "button";
        deleteBtn.className = "delete";
        deleteBtn.textContent = "Delete";
        deleteBtn.addEventListener("click", function () {
          deleteTask(task.id);
          render();
        });

        item.append(label, doneBtn, deleteBtn);
        list.appendChild(item);
      });
      const open = tasks.filter(function (task) { return !task.done; }).length;
      status.textContent = open + " open · " + tasks.length + " total";
    }

    function onAdd() {
      addTask(field.value);
      render();
      field.focus();
    }

    document.getElementById("add").addEventListener("click", onAdd);
    field.addEventListener("keydown", function (event) {
      if (event.key === "Enter") onAdd();
    });
    render();
  </script>
</body>
</html>

Redraw, do not patch

For a list this small, rebuilding every row is cheaper than tracking which node belongs to which id. When you later persist to localStorage (the expense project does that), you already have an array you can serialize.

filter returns a new array. Assign it back to tasks. If you callfilter and ignore the return value, the deleted row comes back on the next render.

Common mistakes

  • Using the loop index as the id. After a delete, Done on row 0 toggles a different task.
  • Calling innerHTML += on each add. Event listeners on old rows disappear.
  • Pushing the input element itself into the array instead of its trimmed string.
  • Forgetting to render after delete. The array is correct. The page still shows the row.
  • Allowing empty tasks. The list fills with blank lines that look like a bug.

Practice tasks

  1. Add a Clear completed button that filters out every task with done === true.
  2. Refuse duplicate text (case-insensitive). Show a status message instead of adding.
  3. Move open tasks above completed tasks in render. Preview at /javascript/try.

FAQ: JavaScript Project: To-Do List

Common questions about this page.

What is the StudyGrid JavaScript tutorial?

The StudyGrid JavaScript tutorial is a full beginner track: variables, functions, arrays, if/else, the DOM, events, storage, and fetch. Each chapter has copy-and-run examples.

Should I run javascript todo project examples locally for better learning?

Yes. Use the browser editor on StudyGrid for a quick check, then Download the example and run it on your computer. Local runs show real errors and the real toolchain, which is one of the fastest ways to learn javascript todo project in this JavaScript JavaScript lesson (JavaScript Project: To-Do List).

Is the JavaScript editor the same as Try Python?

No. Try JavaScript is a live page plus a console at /javascript/try. Try Python stays at /try. JavaScript lessons never open the Python editor.

Do I need to install anything to learn JavaScript?

No. Open a chapter, click Try it in JavaScript, and the page and console update in the browser.

Where should I start the JavaScript tutorial?

Start at JavaScript Intro, then Where To, Output, and Syntax. After variables and functions, continue to the DOM and events. Use Next at the bottom of each chapter.

Is the JavaScript tutorial free?

Yes. The JavaScript studio on StudyGrid (studygrid.in) is free: dashboard, chapters, and the live editor.