Learn
🤖 AI Hub 📰 News 📖 Dictionary 🧰 Resources
Grow
💼 Jobs 🚀 Community 🏆 Challenges
Explore by Stage
🌑 Zero 📚 Learn 🔧 Practice 🏗 Build 🚀 Launch 💰 Earn
Account
Login Sign Up
🏗️HTML 🎨CSS ⚡JavaScript 🐙Git & GitHub ⚛️React Soon 📘TypeScript Soon 🐍Python 🗄️Database & SQL Soon 🌐Web Development Soon ☁️Cloud & DevOps Soon 🤖AI & ML Soon 📄Resume Prep Soon 🎤Interview Prep Soon 💼Freelancing Soon 🚀SaaS & Side Projects Soon 🤖Automation Soon
Advanced 🕑 35 min read 👁 2 views 📅 Oct 21, 2026

Project: Build a Study Planner To-do App

Build a study planner to-do app step by step with plain JavaScript: add, tick, delete and filter tasks, count what is left, and save everything in the browser with localStorage.

The Story: Ananya’s Exam Weeks

Ananya is in her third semester of B.Sc. Her exams start in three weeks. She has five subjects, a pile of notes and a small notebook where she writes tasks like “Revise Physics chapter 3” and “Solve 20 maths problems”. The notebook gets messy fast: tasks are crossed out, rewritten and lost.

In this project you will build Ananya a Study Planner: a small to-do app that runs in the browser. She can add a task, tick it when it is done, delete it, see only pending or done tasks, and see how many tasks are left. Her list is saved in the browser, so it is still there tomorrow.

A to-do app sounds simple, but it uses almost every core JavaScript skill: arrays and objects for the data, DOM methods to draw the list, events for every click, and localStorage with JSON to remember things.

How to follow this project: every step below is a complete page. Press ▶ Try it yourself on each one, play with it, then read what changed. Each step adds one feature to the step before it.

The Plan: Data First, Then the Screen

The most important idea in this project is simple: the array is the truth, and the page is only a picture of the array. We never read tasks back from the screen. Whenever something changes, we change the array, then call one function, render(), that redraws the list from the array.

StepWhat you addMain skill
1The layoutHTML, CSS, querySelector
2Tasks in an array, drawn by render()Arrays of objects, createElement
3Add a taskThe submit event
4Mark done and deleteEvent delegation
5“Tasks left” counterfilter and length
6All / Pending / Done filterApp state
7Save in the browserlocalStorage and JSON

Step 1: Build the Layout

Start with plain HTML and CSS: a card with a heading, a form with one text box and an Add button, and a list. The two list items are written by hand for now, so you can see how the finished list will look.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Study Planner</title>
  <style>
    body { font-family: system-ui, sans-serif; background: #f4f6fb; color: #1f2937; margin: 0; padding: 24px 16px; }
    .planner { max-width: 440px; margin: 0 auto; background: #ffffff; border-radius: 12px; padding: 20px; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08); }
    h1 { font-size: 1.4rem; margin: 0 0 4px; }
    .sub { margin: 0 0 16px; color: #6b7280; }
    form { display: flex; gap: 8px; }
    form input { flex: 1; padding: 10px; border: 1px solid #cbd5e1; border-radius: 8px; font-size: 1rem; }
    button { padding: 10px 14px; border: none; border-radius: 8px; background: #4f46e5; color: #ffffff; font-size: 1rem; cursor: pointer; }
    ul { list-style: none; padding: 0; margin: 16px 0 0; }
    li { display: flex; align-items: center; gap: 10px; padding: 10px 0; border-bottom: 1px solid #e5e7eb; }
    li span { flex: 1; }
  </style>
</head>
<body>
  <div class="planner">
    <h1>My Study Planner</h1>
    <p class="sub">Semester exams are coming. One task at a time.</p>

    <form id="task-form">
      <input id="task-input" type="text" placeholder="For example: Revise Physics chapter 3" aria-label="New study task">
      <button type="submit">Add</button>
    </form>

    <ul id="task-list">
      <li><span>Revise Physics chapter 3</span></li>
      <li><span>Solve 20 maths problems</span></li>
    </ul>
  </div>

  <script>
    const form = document.querySelector('#task-form');
    const input = document.querySelector('#task-input');
    const list = document.querySelector('#task-list');

    console.log('Planner ready. Items on the page:', list.children.length); // 2
  </script>
</body>
</html>

The script only selects the three elements we will use in every step: the form, the input and the list. list.children.length counts the items inside the list, so the console shows 2. Nothing is interactive yet: get the layout right first, then add behaviour.

Notice the button has type="submit" and sits inside the form. That means pressing Enter in the text box also adds a task later, which feels natural on a phone keyboard too.

Step 2: Store Tasks in an Array and Render Them

Now we remove the hand-written items. Each task becomes an object with three properties: an id to identify it, the text, and done, which is true or false. All tasks live in one array called tasks.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Study Planner</title>
  <style>
    body { font-family: system-ui, sans-serif; background: #f4f6fb; color: #1f2937; margin: 0; padding: 24px 16px; }
    .planner { max-width: 440px; margin: 0 auto; background: #ffffff; border-radius: 12px; padding: 20px; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08); }
    h1 { font-size: 1.4rem; margin: 0 0 4px; }
    .sub { margin: 0 0 16px; color: #6b7280; }
    form { display: flex; gap: 8px; }
    form input { flex: 1; padding: 10px; border: 1px solid #cbd5e1; border-radius: 8px; font-size: 1rem; }
    button { padding: 10px 14px; border: none; border-radius: 8px; background: #4f46e5; color: #ffffff; font-size: 1rem; cursor: pointer; }
    ul { list-style: none; padding: 0; margin: 16px 0 0; }
    li { display: flex; align-items: center; gap: 10px; padding: 10px 0; border-bottom: 1px solid #e5e7eb; }
    li span { flex: 1; }
    li.done span { text-decoration: line-through; color: #9ca3af; }
  </style>
</head>
<body>
  <div class="planner">
    <h1>My Study Planner</h1>
    <p class="sub">Semester exams are coming. One task at a time.</p>

    <form id="task-form">
      <input id="task-input" type="text" placeholder="For example: Revise Physics chapter 3" aria-label="New study task">
      <button type="submit">Add</button>
    </form>

    <ul id="task-list"></ul>
  </div>

  <script>
    let tasks = [
      { id: 1, text: 'Revise Physics chapter 3', done: false },
      { id: 2, text: 'Solve 20 maths problems', done: true },
      { id: 3, text: 'Make a formula sheet for Chemistry', done: false }
    ];

    const list = document.querySelector('#task-list');

    function render() {
      list.innerHTML = '';
      for (const task of tasks) {
        const li = document.createElement('li');
        li.classList.toggle('done', task.done);
        const span = document.createElement('span');
        span.textContent = task.text;
        li.append(span);
        list.append(li);
      }
    }

    render();
  </script>
</body>
</html>

Line by line, here is what render() does:

  • list.innerHTML = '' empties the list, so we always start from a clean slate.
  • for (const task of tasks) visits every object in the array.
  • document.createElement('li') makes a new list item in memory.
  • li.classList.toggle('done', task.done) adds the class done only when the second argument is true. The CSS then draws a line through finished tasks.
  • span.textContent = task.text puts the task text in, as plain text.
  • append puts the span inside the item and the item inside the list.

We use let for tasks because in Step 4 we will replace the whole array when deleting. Set the first task’s done to true and run again: the line appears.

Step 3: Add a Task with the Form

Ananya types a task and presses Add. We listen for the form’s submit event, not the button’s click, because submit also fires when she presses Enter.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Study Planner</title>
  <style>
    body { font-family: system-ui, sans-serif; background: #f4f6fb; color: #1f2937; margin: 0; padding: 24px 16px; }
    .planner { max-width: 440px; margin: 0 auto; background: #ffffff; border-radius: 12px; padding: 20px; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08); }
    h1 { font-size: 1.4rem; margin: 0 0 4px; }
    .sub { margin: 0 0 16px; color: #6b7280; }
    form { display: flex; gap: 8px; }
    form input { flex: 1; padding: 10px; border: 1px solid #cbd5e1; border-radius: 8px; font-size: 1rem; }
    button { padding: 10px 14px; border: none; border-radius: 8px; background: #4f46e5; color: #ffffff; font-size: 1rem; cursor: pointer; }
    ul { list-style: none; padding: 0; margin: 16px 0 0; }
    li { display: flex; align-items: center; gap: 10px; padding: 10px 0; border-bottom: 1px solid #e5e7eb; }
    li span { flex: 1; }
    li.done span { text-decoration: line-through; color: #9ca3af; }
  </style>
</head>
<body>
  <div class="planner">
    <h1>My Study Planner</h1>
    <p class="sub">Semester exams are coming. One task at a time.</p>

    <form id="task-form">
      <input id="task-input" type="text" placeholder="For example: Revise Physics chapter 3" aria-label="New study task">
      <button type="submit">Add</button>
    </form>

    <ul id="task-list"></ul>
  </div>

  <script>
    let tasks = [
      { id: 1, text: 'Revise Physics chapter 3', done: false },
      { id: 2, text: 'Solve 20 maths problems', done: true },
      { id: 3, text: 'Make a formula sheet for Chemistry', done: false }
    ];

    const form = document.querySelector('#task-form');
    const input = document.querySelector('#task-input');
    const list = document.querySelector('#task-list');

    function render() {
      list.innerHTML = '';
      for (const task of tasks) {
        const li = document.createElement('li');
        li.classList.toggle('done', task.done);
        const span = document.createElement('span');
        span.textContent = task.text;
        li.append(span);
        list.append(li);
      }
    }

    form.addEventListener('submit', (event) => {
      event.preventDefault();
      const text = input.value.trim();
      if (text === '') {
        input.focus();
        return;
      }
      tasks.push({ id: Date.now(), text: text, done: false });
      input.value = '';
      input.focus();
      render();
    });

    render();
  </script>
</body>
</html>
  • event.preventDefault() stops the browser’s normal behaviour, which is to send the form and reload the page. Without it, the new task would vanish at once.
  • input.value.trim() reads the text and removes spaces at the start and end. If the result is an empty string, we put the cursor back in the box and return, so empty tasks are ignored.
  • Date.now() gives the number of milliseconds since 1 January 1970. It is different every time you click, so it works well as a simple unique id.
  • After push, we clear the box, keep the focus in it for the next task, and call render().

Try pressing Add with only spaces in the box. Nothing happens, which is exactly what we want.

Step 4: Mark Done and Delete with Event Delegation

Each row now gets a checkbox and a Delete button. Here is the tricky part: render() throws away all rows and makes new ones every time. If we attached a click listener to each button, those listeners would be thrown away too, and we would need to attach them again after every render.

The clean solution is event delegation. Clicks bubble up from the element you clicked to its parents. So we put one listener on the <ul>, which never changes, and inside it we check what was clicked. You met bubbling in the JavaScript Events lesson.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Study Planner</title>
  <style>
    body { font-family: system-ui, sans-serif; background: #f4f6fb; color: #1f2937; margin: 0; padding: 24px 16px; }
    .planner { max-width: 440px; margin: 0 auto; background: #ffffff; border-radius: 12px; padding: 20px; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08); }
    h1 { font-size: 1.4rem; margin: 0 0 4px; }
    .sub { margin: 0 0 16px; color: #6b7280; }
    form { display: flex; gap: 8px; }
    form input { flex: 1; padding: 10px; border: 1px solid #cbd5e1; border-radius: 8px; font-size: 1rem; }
    button { padding: 10px 14px; border: none; border-radius: 8px; background: #4f46e5; color: #ffffff; font-size: 1rem; cursor: pointer; }
    ul { list-style: none; padding: 0; margin: 16px 0 0; }
    li { display: flex; align-items: center; gap: 10px; padding: 10px 0; border-bottom: 1px solid #e5e7eb; }
    li span { flex: 1; }
    li.done span { text-decoration: line-through; color: #9ca3af; }
    li input { width: 18px; height: 18px; }
    .delete { background: #fee2e2; color: #b91c1c; padding: 6px 10px; font-size: 0.9rem; }
  </style>
</head>
<body>
  <div class="planner">
    <h1>My Study Planner</h1>
    <p class="sub">Semester exams are coming. One task at a time.</p>

    <form id="task-form">
      <input id="task-input" type="text" placeholder="For example: Revise Physics chapter 3" aria-label="New study task">
      <button type="submit">Add</button>
    </form>

    <ul id="task-list"></ul>
  </div>

  <script>
    let tasks = [
      { id: 1, text: 'Revise Physics chapter 3', done: false },
      { id: 2, text: 'Solve 20 maths problems', done: true },
      { id: 3, text: 'Make a formula sheet for Chemistry', done: false }
    ];

    const form = document.querySelector('#task-form');
    const input = document.querySelector('#task-input');
    const list = document.querySelector('#task-list');

    function render() {
      list.innerHTML = '';
      for (const task of tasks) {
        const li = document.createElement('li');
        li.dataset.id = task.id;
        li.classList.toggle('done', task.done);

        const check = document.createElement('input');
        check.type = 'checkbox';
        check.className = 'toggle';
        check.checked = task.done;
        check.setAttribute('aria-label', 'Mark as done');

        const span = document.createElement('span');
        span.textContent = task.text;

        const del = document.createElement('button');
        del.type = 'button';
        del.className = 'delete';
        del.textContent = 'Delete';

        li.append(check, span, del);
        list.append(li);
      }
    }

    form.addEventListener('submit', (event) => {
      event.preventDefault();
      const text = input.value.trim();
      if (text === '') {
        input.focus();
        return;
      }
      tasks.push({ id: Date.now(), text: text, done: false });
      input.value = '';
      input.focus();
      render();
    });

    list.addEventListener('click', (event) => {
      const li = event.target.closest('li');
      if (!li) return;
      const id = Number(li.dataset.id);

      if (event.target.classList.contains('toggle')) {
        const task = tasks.find((t) => t.id === id);
        task.done = !task.done;
      } else if (event.target.classList.contains('delete')) {
        tasks = tasks.filter((t) => t.id !== id);
      } else {
        return;
      }
      render();
    });

    render();
  </script>
</body>
</html>
  • li.dataset.id = task.id writes a data-id attribute on each row, so the row remembers which task it shows.
  • event.target is the exact element clicked. closest('li') walks up to the nearest row.
  • Number(li.dataset.id) turns the id back into a number. Attributes are always strings, and '5' === 5 is false.
  • For the checkbox, find returns the matching task object and we flip done with !.
  • For Delete, filter builds a new array with every task except the clicked one. That is why tasks uses let.
  • A click on the text itself matches neither class, so we return without redrawing.

Rows added later work straight away, because the one listener on the list handles them all.

Step 5: Show How Many Tasks Are Left

Ananya wants a quick answer to “How much is left?”. We add a paragraph under the list and update it at the end of render().

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Study Planner</title>
  <style>
    body { font-family: system-ui, sans-serif; background: #f4f6fb; color: #1f2937; margin: 0; padding: 24px 16px; }
    .planner { max-width: 440px; margin: 0 auto; background: #ffffff; border-radius: 12px; padding: 20px; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08); }
    h1 { font-size: 1.4rem; margin: 0 0 4px; }
    .sub { margin: 0 0 16px; color: #6b7280; }
    form { display: flex; gap: 8px; }
    form input { flex: 1; padding: 10px; border: 1px solid #cbd5e1; border-radius: 8px; font-size: 1rem; }
    button { padding: 10px 14px; border: none; border-radius: 8px; background: #4f46e5; color: #ffffff; font-size: 1rem; cursor: pointer; }
    ul { list-style: none; padding: 0; margin: 16px 0 0; }
    li { display: flex; align-items: center; gap: 10px; padding: 10px 0; border-bottom: 1px solid #e5e7eb; }
    li span { flex: 1; }
    li.done span { text-decoration: line-through; color: #9ca3af; }
    li input { width: 18px; height: 18px; }
    .delete { background: #fee2e2; color: #b91c1c; padding: 6px 10px; font-size: 0.9rem; }
    .footer { margin: 12px 0 0; color: #6b7280; font-size: 0.95rem; }
  </style>
</head>
<body>
  <div class="planner">
    <h1>My Study Planner</h1>
    <p class="sub">Semester exams are coming. One task at a time.</p>

    <form id="task-form">
      <input id="task-input" type="text" placeholder="For example: Revise Physics chapter 3" aria-label="New study task">
      <button type="submit">Add</button>
    </form>

    <ul id="task-list"></ul>
    <p class="footer" id="count"></p>
  </div>

  <script>
    let tasks = [
      { id: 1, text: 'Revise Physics chapter 3', done: false },
      { id: 2, text: 'Solve 20 maths problems', done: true },
      { id: 3, text: 'Make a formula sheet for Chemistry', done: false }
    ];

    const form = document.querySelector('#task-form');
    const input = document.querySelector('#task-input');
    const list = document.querySelector('#task-list');
    const count = document.querySelector('#count');

    function render() {
      list.innerHTML = '';
      for (const task of tasks) {
        const li = document.createElement('li');
        li.dataset.id = task.id;
        li.classList.toggle('done', task.done);

        const check = document.createElement('input');
        check.type = 'checkbox';
        check.className = 'toggle';
        check.checked = task.done;
        check.setAttribute('aria-label', 'Mark as done');

        const span = document.createElement('span');
        span.textContent = task.text;

        const del = document.createElement('button');
        del.type = 'button';
        del.className = 'delete';
        del.textContent = 'Delete';

        li.append(check, span, del);
        list.append(li);
      }
      const left = tasks.filter((t) => !t.done).length;
      count.textContent = left === 1 ? '1 task left' : `${left} tasks left`;
    }

    form.addEventListener('submit', (event) => {
      event.preventDefault();
      const text = input.value.trim();
      if (text === '') {
        input.focus();
        return;
      }
      tasks.push({ id: Date.now(), text: text, done: false });
      input.value = '';
      input.focus();
      render();
    });

    list.addEventListener('click', (event) => {
      const li = event.target.closest('li');
      if (!li) return;
      const id = Number(li.dataset.id);

      if (event.target.classList.contains('toggle')) {
        const task = tasks.find((t) => t.id === id);
        task.done = !task.done;
      } else if (event.target.classList.contains('delete')) {
        tasks = tasks.filter((t) => t.id !== id);
      } else {
        return;
      }
      render();
    });

    render();
  </script>
</body>
</html>

tasks.filter((t) => !t.done) keeps only the unfinished tasks, and .length counts them. The conditional (ternary) operator picks the right words: “1 task left” for one, and a template string such as “2 tasks left” for any other number, including zero.

Adding, ticking and deleting all call render(), so the counter is always correct.

Step 6: Filter: All, Pending, Done

On the night before an exam, Ananya only wants to see what is still pending. We add three filter buttons and one new piece of state: the variable currentFilter, which holds 'all', 'pending' or 'done'.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Study Planner</title>
  <style>
    body { font-family: system-ui, sans-serif; background: #f4f6fb; color: #1f2937; margin: 0; padding: 24px 16px; }
    .planner { max-width: 440px; margin: 0 auto; background: #ffffff; border-radius: 12px; padding: 20px; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08); }
    h1 { font-size: 1.4rem; margin: 0 0 4px; }
    .sub { margin: 0 0 16px; color: #6b7280; }
    form { display: flex; gap: 8px; }
    form input { flex: 1; padding: 10px; border: 1px solid #cbd5e1; border-radius: 8px; font-size: 1rem; }
    button { padding: 10px 14px; border: none; border-radius: 8px; background: #4f46e5; color: #ffffff; font-size: 1rem; cursor: pointer; }
    ul { list-style: none; padding: 0; margin: 16px 0 0; }
    li { display: flex; align-items: center; gap: 10px; padding: 10px 0; border-bottom: 1px solid #e5e7eb; }
    li span { flex: 1; }
    li.done span { text-decoration: line-through; color: #9ca3af; }
    li input { width: 18px; height: 18px; }
    .delete { background: #fee2e2; color: #b91c1c; padding: 6px 10px; font-size: 0.9rem; }
    .footer { margin: 12px 0 0; color: #6b7280; font-size: 0.95rem; }
    .filters { display: flex; gap: 6px; margin-top: 16px; }
    .filters button { background: #e5e7eb; color: #1f2937; padding: 6px 12px; font-size: 0.9rem; }
    .filters button.active { background: #4f46e5; color: #ffffff; }
    li.empty { color: #6b7280; justify-content: center; }
  </style>
</head>
<body>
  <div class="planner">
    <h1>My Study Planner</h1>
    <p class="sub">Semester exams are coming. One task at a time.</p>

    <form id="task-form">
      <input id="task-input" type="text" placeholder="For example: Revise Physics chapter 3" aria-label="New study task">
      <button type="submit">Add</button>
    </form>

    <div class="filters" id="filters">
      <button type="button" data-filter="all" class="active">All</button>
      <button type="button" data-filter="pending">Pending</button>
      <button type="button" data-filter="done">Done</button>
    </div>

    <ul id="task-list"></ul>
    <p class="footer" id="count"></p>
  </div>

  <script>
    let tasks = [
      { id: 1, text: 'Revise Physics chapter 3', done: false },
      { id: 2, text: 'Solve 20 maths problems', done: true },
      { id: 3, text: 'Make a formula sheet for Chemistry', done: false }
    ];
    let currentFilter = 'all';

    const form = document.querySelector('#task-form');
    const input = document.querySelector('#task-input');
    const list = document.querySelector('#task-list');
    const count = document.querySelector('#count');
    const filters = document.querySelector('#filters');

    function visibleTasks() {
      if (currentFilter === 'pending') return tasks.filter((t) => !t.done);
      if (currentFilter === 'done') return tasks.filter((t) => t.done);
      return tasks;
    }

    function render() {
      list.innerHTML = '';
      const shown = visibleTasks();

      if (shown.length === 0) {
        const li = document.createElement('li');
        li.className = 'empty';
        li.textContent = 'Nothing here. Enjoy a short break!';
        list.append(li);
      }

      for (const task of shown) {
        const li = document.createElement('li');
        li.dataset.id = task.id;
        li.classList.toggle('done', task.done);

        const check = document.createElement('input');
        check.type = 'checkbox';
        check.className = 'toggle';
        check.checked = task.done;
        check.setAttribute('aria-label', 'Mark as done');

        const span = document.createElement('span');
        span.textContent = task.text;

        const del = document.createElement('button');
        del.type = 'button';
        del.className = 'delete';
        del.textContent = 'Delete';

        li.append(check, span, del);
        list.append(li);
      }

      const left = tasks.filter((t) => !t.done).length;
      count.textContent = left === 1 ? '1 task left' : `${left} tasks left`;
    }

    form.addEventListener('submit', (event) => {
      event.preventDefault();
      const text = input.value.trim();
      if (text === '') {
        input.focus();
        return;
      }
      tasks.push({ id: Date.now(), text: text, done: false });
      input.value = '';
      input.focus();
      render();
    });

    list.addEventListener('click', (event) => {
      const li = event.target.closest('li');
      if (!li) return;
      const id = Number(li.dataset.id);

      if (event.target.classList.contains('toggle')) {
        const task = tasks.find((t) => t.id === id);
        task.done = !task.done;
      } else if (event.target.classList.contains('delete')) {
        tasks = tasks.filter((t) => t.id !== id);
      } else {
        return;
      }
      render();
    });

    filters.addEventListener('click', (event) => {
      const button = event.target.closest('button');
      if (!button) return;
      currentFilter = button.dataset.filter;
      for (const b of filters.querySelectorAll('button')) {
        b.classList.toggle('active', b === button);
      }
      render();
    });

    render();
  </script>
</body>
</html>
  • visibleTasks() returns the tasks to show. The tasks array itself is never changed by filtering: we only choose which items to draw.
  • The filter buttons use delegation too. Each button has a data-filter attribute, and button.dataset.filter reads it.
  • The loop gives the active class to the clicked button and removes it from the others, so the learner can see which filter is on.
  • When nothing matches, we show a friendly “Nothing here” row instead of an empty box. It has no data-id, and the list’s click handler simply ignores it.

The counter still counts all pending tasks, not just the visible ones, because it uses tasks, not shown.

Step 7: The Complete App, Saved with localStorage

Right now, closing the tab loses everything. localStorage is a small storage space in the browser that keeps text even after you close the browser. It can only store strings, so we convert the array to JSON text with JSON.stringify when saving, and back into an array with JSON.parse when loading. The JavaScript Local Storage lesson covers this in detail.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Study Planner</title>
  <style>
    body { font-family: system-ui, sans-serif; background: #f4f6fb; color: #1f2937; margin: 0; padding: 24px 16px; }
    .planner { max-width: 440px; margin: 0 auto; background: #ffffff; border-radius: 12px; padding: 20px; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08); }
    h1 { font-size: 1.4rem; margin: 0 0 4px; }
    .sub { margin: 0 0 16px; color: #6b7280; }
    form { display: flex; gap: 8px; }
    form input { flex: 1; padding: 10px; border: 1px solid #cbd5e1; border-radius: 8px; font-size: 1rem; }
    button { padding: 10px 14px; border: none; border-radius: 8px; background: #4f46e5; color: #ffffff; font-size: 1rem; cursor: pointer; }
    ul { list-style: none; padding: 0; margin: 16px 0 0; }
    li { display: flex; align-items: center; gap: 10px; padding: 10px 0; border-bottom: 1px solid #e5e7eb; }
    li span { flex: 1; }
    li.done span { text-decoration: line-through; color: #9ca3af; }
    li input { width: 18px; height: 18px; }
    .delete { background: #fee2e2; color: #b91c1c; padding: 6px 10px; font-size: 0.9rem; }
    .footer { margin: 12px 0 0; color: #6b7280; font-size: 0.95rem; }
    .filters { display: flex; gap: 6px; margin-top: 16px; }
    .filters button { background: #e5e7eb; color: #1f2937; padding: 6px 12px; font-size: 0.9rem; }
    .filters button.active { background: #4f46e5; color: #ffffff; }
    li.empty { color: #6b7280; justify-content: center; }
  </style>
</head>
<body>
  <div class="planner">
    <h1>My Study Planner</h1>
    <p class="sub">Semester exams are coming. One task at a time.</p>

    <form id="task-form">
      <input id="task-input" type="text" placeholder="For example: Revise Physics chapter 3" aria-label="New study task">
      <button type="submit">Add</button>
    </form>

    <div class="filters" id="filters">
      <button type="button" data-filter="all" class="active">All</button>
      <button type="button" data-filter="pending">Pending</button>
      <button type="button" data-filter="done">Done</button>
    </div>

    <ul id="task-list"></ul>
    <p class="footer" id="count"></p>
  </div>

  <script>
    const STORAGE_KEY = 'study-planner-tasks';

    function loadTasks() {
      const saved = localStorage.getItem(STORAGE_KEY);
      if (!saved) return [];
      try {
        return JSON.parse(saved);
      } catch {
        return [];
      }
    }

    function saveTasks() {
      localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks));
    }

    function update() {
      saveTasks();
      render();
    }

    let tasks = loadTasks();
    let currentFilter = 'all';

    const form = document.querySelector('#task-form');
    const input = document.querySelector('#task-input');
    const list = document.querySelector('#task-list');
    const count = document.querySelector('#count');
    const filters = document.querySelector('#filters');

    function visibleTasks() {
      if (currentFilter === 'pending') return tasks.filter((t) => !t.done);
      if (currentFilter === 'done') return tasks.filter((t) => t.done);
      return tasks;
    }

    function render() {
      list.innerHTML = '';
      const shown = visibleTasks();

      if (shown.length === 0) {
        const li = document.createElement('li');
        li.className = 'empty';
        li.textContent = 'Nothing here. Enjoy a short break!';
        list.append(li);
      }

      for (const task of shown) {
        const li = document.createElement('li');
        li.dataset.id = task.id;
        li.classList.toggle('done', task.done);

        const check = document.createElement('input');
        check.type = 'checkbox';
        check.className = 'toggle';
        check.checked = task.done;
        check.setAttribute('aria-label', 'Mark as done');

        const span = document.createElement('span');
        span.textContent = task.text;

        const del = document.createElement('button');
        del.type = 'button';
        del.className = 'delete';
        del.textContent = 'Delete';

        li.append(check, span, del);
        list.append(li);
      }

      const left = tasks.filter((t) => !t.done).length;
      count.textContent = left === 1 ? '1 task left' : `${left} tasks left`;
    }

    form.addEventListener('submit', (event) => {
      event.preventDefault();
      const text = input.value.trim();
      if (text === '') {
        input.focus();
        return;
      }
      tasks.push({ id: Date.now(), text: text, done: false });
      input.value = '';
      input.focus();
      update();
    });

    list.addEventListener('click', (event) => {
      const li = event.target.closest('li');
      if (!li) return;
      const id = Number(li.dataset.id);

      if (event.target.classList.contains('toggle')) {
        const task = tasks.find((t) => t.id === id);
        task.done = !task.done;
      } else if (event.target.classList.contains('delete')) {
        tasks = tasks.filter((t) => t.id !== id);
      } else {
        return;
      }
      update();
    });

    filters.addEventListener('click', (event) => {
      const button = event.target.closest('button');
      if (!button) return;
      currentFilter = button.dataset.filter;
      for (const b of filters.querySelectorAll('button')) {
        b.classList.toggle('active', b === button);
      }
      render();
    });

    render();
  </script>
</body>
</html>
  • loadTasks() reads the saved text. The first time, there is nothing saved, so getItem returns null and we start with an empty array.
  • The try...catch protects us if the saved text is broken: instead of crashing, the app starts empty.
  • update() saves and then renders. We call it after every change to the data (add, tick, delete). Changing the filter only calls render(), because the data did not change.
  • STORAGE_KEY is the name of the saved item. A clear, unique name avoids clashes with other apps.
About the Try it yourself box: the playground gives you a temporary, in-memory localStorage. Your tasks survive while you use the box, but they disappear when you reset or reload it. Save the code as index.html on your computer and open it in Chrome or Firefox to see tasks stay after closing the browser.

Real-Life Example: Ananya’s Planner in Use

On Monday Ananya adds “Revise Physics chapter 3”, “Solve 20 maths problems” and “Read English poem notes”. The counter says 3 tasks left. She finishes maths, ticks it, and the line appears. On Tuesday she opens the same file, and the list is still there, because it was saved. Before sleeping she picks Pending to see only what remains. This is the complete app from Step 7.

Try these changes:

  1. Change the placeholder text and the heading to suit your own subjects.
  2. Change the empty message to show a different line for each filter, for example “No finished tasks yet” when the Done filter is on.
  3. Show the total count too, such as “2 of 5 tasks left”.

Ideas to Take It Further

  • Subjects: add a <select> for the subject (Physics, Maths, English) and show it as a small coloured label on each task.
  • Due dates: add an <input type="date"> and sort tasks so the nearest date comes first.
  • Edit a task: on double-click, swap the text for an input box and save the new text on Enter.
  • Progress bar: show the percentage of tasks done as a bar that fills up.

Common Mistakes

  • Forgetting event.preventDefault(). The form reloads the page and the new task disappears in a flash. Fix: call it as the first line of the submit handler.
  • Adding listeners inside render() or to each button. The rows are rebuilt on every render, so listeners get lost or added twice. Fix: one delegated listener on the list, added once.
  • Comparing a string id with a number id. dataset always gives strings, so find returns undefined and the next line throws an error. Fix: convert with Number(), as this example shows:
// dataset values are always strings
const idFromPage = '1712345';
const idInArray = 1712345;

console.log(idFromPage === idInArray);         // false
console.log(Number(idFromPage) === idInArray); // true
  • Saving the array without JSON. localStorage.setItem('tasks', tasks) saves the text [object Object], and your data is lost. Fix: always JSON.stringify before saving and JSON.parse after loading.
  • Using innerHTML for the task text. If someone types <b> or a script tag, the browser treats it as HTML. Fix: use textContent for anything the user typed, as we did.

Practice Task: Add a “Clear completed” Button

After a good study day, Ananya has many ticked tasks. Add a Clear completed button that removes every done task in one click. The button is already in the HTML below. Complete the two TODO lines in the script.

  • Select the button with the id clear-done.
  • On click, keep only the tasks where done is false.
  • Save and redraw by calling update().
  • Check that the counter and the filter still work afterwards.
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Study Planner</title>
  <style>
    body { font-family: system-ui, sans-serif; background: #f4f6fb; color: #1f2937; margin: 0; padding: 24px 16px; }
    .planner { max-width: 440px; margin: 0 auto; background: #ffffff; border-radius: 12px; padding: 20px; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08); }
    h1 { font-size: 1.4rem; margin: 0 0 4px; }
    .sub { margin: 0 0 16px; color: #6b7280; }
    form { display: flex; gap: 8px; }
    form input { flex: 1; padding: 10px; border: 1px solid #cbd5e1; border-radius: 8px; font-size: 1rem; }
    button { padding: 10px 14px; border: none; border-radius: 8px; background: #4f46e5; color: #ffffff; font-size: 1rem; cursor: pointer; }
    ul { list-style: none; padding: 0; margin: 16px 0 0; }
    li { display: flex; align-items: center; gap: 10px; padding: 10px 0; border-bottom: 1px solid #e5e7eb; }
    li span { flex: 1; }
    li.done span { text-decoration: line-through; color: #9ca3af; }
    li input { width: 18px; height: 18px; }
    .delete { background: #fee2e2; color: #b91c1c; padding: 6px 10px; font-size: 0.9rem; }
    .footer { margin: 12px 0 0; color: #6b7280; font-size: 0.95rem; }
    .filters { display: flex; gap: 6px; margin-top: 16px; }
    .filters button { background: #e5e7eb; color: #1f2937; padding: 6px 12px; font-size: 0.9rem; }
    .filters button.active { background: #4f46e5; color: #ffffff; }
    li.empty { color: #6b7280; justify-content: center; }
    .clear { background: transparent; color: #4f46e5; padding: 4px 0; font-size: 0.95rem; }
  </style>
</head>
<body>
  <div class="planner">
    <h1>My Study Planner</h1>
    <p class="sub">Semester exams are coming. One task at a time.</p>

    <form id="task-form">
      <input id="task-input" type="text" placeholder="For example: Revise Physics chapter 3" aria-label="New study task">
      <button type="submit">Add</button>
    </form>

    <div class="filters" id="filters">
      <button type="button" data-filter="all" class="active">All</button>
      <button type="button" data-filter="pending">Pending</button>
      <button type="button" data-filter="done">Done</button>
    </div>

    <ul id="task-list"></ul>
    <p class="footer"><span id="count"></span> &middot; <button type="button" class="clear" id="clear-done">Clear completed</button></p>
  </div>

  <script>
    const STORAGE_KEY = 'study-planner-tasks';

    function loadTasks() {
      const saved = localStorage.getItem(STORAGE_KEY);
      if (!saved) return [];
      try {
        return JSON.parse(saved);
      } catch {
        return [];
      }
    }

    function saveTasks() {
      localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks));
    }

    function update() {
      saveTasks();
      render();
    }

    let tasks = loadTasks();
    let currentFilter = 'all';

    const form = document.querySelector('#task-form');
    const input = document.querySelector('#task-input');
    const list = document.querySelector('#task-list');
    const count = document.querySelector('#count');
    const filters = document.querySelector('#filters');

    function visibleTasks() {
      if (currentFilter === 'pending') return tasks.filter((t) => !t.done);
      if (currentFilter === 'done') return tasks.filter((t) => t.done);
      return tasks;
    }

    function render() {
      list.innerHTML = '';
      const shown = visibleTasks();

      if (shown.length === 0) {
        const li = document.createElement('li');
        li.className = 'empty';
        li.textContent = 'Nothing here. Enjoy a short break!';
        list.append(li);
      }

      for (const task of shown) {
        const li = document.createElement('li');
        li.dataset.id = task.id;
        li.classList.toggle('done', task.done);

        const check = document.createElement('input');
        check.type = 'checkbox';
        check.className = 'toggle';
        check.checked = task.done;
        check.setAttribute('aria-label', 'Mark as done');

        const span = document.createElement('span');
        span.textContent = task.text;

        const del = document.createElement('button');
        del.type = 'button';
        del.className = 'delete';
        del.textContent = 'Delete';

        li.append(check, span, del);
        list.append(li);
      }

      const left = tasks.filter((t) => !t.done).length;
      count.textContent = left === 1 ? '1 task left' : `${left} tasks left`;
    }

    form.addEventListener('submit', (event) => {
      event.preventDefault();
      const text = input.value.trim();
      if (text === '') {
        input.focus();
        return;
      }
      tasks.push({ id: Date.now(), text: text, done: false });
      input.value = '';
      input.focus();
      update();
    });

    list.addEventListener('click', (event) => {
      const li = event.target.closest('li');
      if (!li) return;
      const id = Number(li.dataset.id);

      if (event.target.classList.contains('toggle')) {
        const task = tasks.find((t) => t.id === id);
        task.done = !task.done;
      } else if (event.target.classList.contains('delete')) {
        tasks = tasks.filter((t) => t.id !== id);
      } else {
        return;
      }
      update();
    });

    filters.addEventListener('click', (event) => {
      const button = event.target.closest('button');
      if (!button) return;
      currentFilter = button.dataset.filter;
      for (const b of filters.querySelectorAll('button')) {
        b.classList.toggle('active', b === button);
      }
      render();
    });

    // TODO 1: select the #clear-done button.
    // TODO 2: on click, keep only the tasks that are not done, then call update().

    render();
  </script>
</body>
</html>
Show one possible answer (try it yourself first!)
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Study Planner</title>
  <style>
    body { font-family: system-ui, sans-serif; background: #f4f6fb; color: #1f2937; margin: 0; padding: 24px 16px; }
    .planner { max-width: 440px; margin: 0 auto; background: #ffffff; border-radius: 12px; padding: 20px; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08); }
    h1 { font-size: 1.4rem; margin: 0 0 4px; }
    .sub { margin: 0 0 16px; color: #6b7280; }
    form { display: flex; gap: 8px; }
    form input { flex: 1; padding: 10px; border: 1px solid #cbd5e1; border-radius: 8px; font-size: 1rem; }
    button { padding: 10px 14px; border: none; border-radius: 8px; background: #4f46e5; color: #ffffff; font-size: 1rem; cursor: pointer; }
    ul { list-style: none; padding: 0; margin: 16px 0 0; }
    li { display: flex; align-items: center; gap: 10px; padding: 10px 0; border-bottom: 1px solid #e5e7eb; }
    li span { flex: 1; }
    li.done span { text-decoration: line-through; color: #9ca3af; }
    li input { width: 18px; height: 18px; }
    .delete { background: #fee2e2; color: #b91c1c; padding: 6px 10px; font-size: 0.9rem; }
    .footer { margin: 12px 0 0; color: #6b7280; font-size: 0.95rem; }
    .filters { display: flex; gap: 6px; margin-top: 16px; }
    .filters button { background: #e5e7eb; color: #1f2937; padding: 6px 12px; font-size: 0.9rem; }
    .filters button.active { background: #4f46e5; color: #ffffff; }
    li.empty { color: #6b7280; justify-content: center; }
    .clear { background: transparent; color: #4f46e5; padding: 4px 0; font-size: 0.95rem; }
  </style>
</head>
<body>
  <div class="planner">
    <h1>My Study Planner</h1>
    <p class="sub">Semester exams are coming. One task at a time.</p>

    <form id="task-form">
      <input id="task-input" type="text" placeholder="For example: Revise Physics chapter 3" aria-label="New study task">
      <button type="submit">Add</button>
    </form>

    <div class="filters" id="filters">
      <button type="button" data-filter="all" class="active">All</button>
      <button type="button" data-filter="pending">Pending</button>
      <button type="button" data-filter="done">Done</button>
    </div>

    <ul id="task-list"></ul>
    <p class="footer"><span id="count"></span> &middot; <button type="button" class="clear" id="clear-done">Clear completed</button></p>
  </div>

  <script>
    const STORAGE_KEY = 'study-planner-tasks';

    function loadTasks() {
      const saved = localStorage.getItem(STORAGE_KEY);
      if (!saved) return [];
      try {
        return JSON.parse(saved);
      } catch {
        return [];
      }
    }

    function saveTasks() {
      localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks));
    }

    function update() {
      saveTasks();
      render();
    }

    let tasks = loadTasks();
    let currentFilter = 'all';

    const form = document.querySelector('#task-form');
    const input = document.querySelector('#task-input');
    const list = document.querySelector('#task-list');
    const count = document.querySelector('#count');
    const filters = document.querySelector('#filters');

    function visibleTasks() {
      if (currentFilter === 'pending') return tasks.filter((t) => !t.done);
      if (currentFilter === 'done') return tasks.filter((t) => t.done);
      return tasks;
    }

    function render() {
      list.innerHTML = '';
      const shown = visibleTasks();

      if (shown.length === 0) {
        const li = document.createElement('li');
        li.className = 'empty';
        li.textContent = 'Nothing here. Enjoy a short break!';
        list.append(li);
      }

      for (const task of shown) {
        const li = document.createElement('li');
        li.dataset.id = task.id;
        li.classList.toggle('done', task.done);

        const check = document.createElement('input');
        check.type = 'checkbox';
        check.className = 'toggle';
        check.checked = task.done;
        check.setAttribute('aria-label', 'Mark as done');

        const span = document.createElement('span');
        span.textContent = task.text;

        const del = document.createElement('button');
        del.type = 'button';
        del.className = 'delete';
        del.textContent = 'Delete';

        li.append(check, span, del);
        list.append(li);
      }

      const left = tasks.filter((t) => !t.done).length;
      count.textContent = left === 1 ? '1 task left' : `${left} tasks left`;
    }

    form.addEventListener('submit', (event) => {
      event.preventDefault();
      const text = input.value.trim();
      if (text === '') {
        input.focus();
        return;
      }
      tasks.push({ id: Date.now(), text: text, done: false });
      input.value = '';
      input.focus();
      update();
    });

    list.addEventListener('click', (event) => {
      const li = event.target.closest('li');
      if (!li) return;
      const id = Number(li.dataset.id);

      if (event.target.classList.contains('toggle')) {
        const task = tasks.find((t) => t.id === id);
        task.done = !task.done;
      } else if (event.target.classList.contains('delete')) {
        tasks = tasks.filter((t) => t.id !== id);
      } else {
        return;
      }
      update();
    });

    filters.addEventListener('click', (event) => {
      const button = event.target.closest('button');
      if (!button) return;
      currentFilter = button.dataset.filter;
      for (const b of filters.querySelectorAll('button')) {
        b.classList.toggle('active', b === button);
      }
      render();
    });

    const clearButton = document.querySelector('#clear-done');

    clearButton.addEventListener('click', () => {
      tasks = tasks.filter((t) => !t.done);
      update();
    });

    render();
  </script>
</body>
</html>

Key Takeaways

  • Keep your data in an array of objects and treat the page as a picture of that array: change the data, then call render().
  • Listen for submit on the form, call preventDefault(), and use trim() to ignore empty input.
  • Event delegation means one listener on a parent handles clicks on children, even children added later. Use closest() and data- attributes to find what was clicked.
  • filter both removes items (delete) and selects items (pending or done) without changing the original logic.
  • localStorage stores only strings, so save with JSON.stringify and load with JSON.parse.
  • Use textContent, not innerHTML, for text the user typed.

❓ Frequently Asked Questions

Redrawing from the array is simpler and avoids bugs where the page and the data disagree. For a list of a few hundred tasks it is fast enough. Large apps and frameworks use smarter ways to update only what changed.
For one person adding tasks by hand, yes, because two clicks never happen in the same millisecond. For data shared between many users, apps use ids from a server or crypto.randomUUID().
Inside your browser, on your own device, separately for each website. Another browser or another phone will not see them, and clearing your browser data removes them.
The playground uses a temporary in-memory storage that is cleared when you reset or reload it. Save the code as an HTML file on your computer to see real saving.
Was this lesson helpful?

🎯 Finished this lesson?

🔗 Related Tutorials
🤖 Got a Doubt? Ask AI
Get instant answers in Hindi or English
🤖
Z2T Bot
Online
Ask me anything about this tutorial — I'm here! 👋