Skip to content
beginner

Make a Simple To-Do List

A to-do list is the first app where your page stops being a static document and starts remembering what the user does. Same event-listening idea as a click…

Published 2026-09-06Updated 2026-09-1212 min read
Asian students in uniform learning in a computer lab, focused on their tasks.
Asian students in uniform learning in a computer lab, focused on their tasks. Photo by Thành Đỗ on Pexels.

A to-do list is the first app where your page stops being a static document and starts remembering what the user does. Same event-listening idea as a click counter—but now the page has to hold a growing list of items. That one shift introduces two ideas you'll use for the rest of your JavaScript life: arrays and re-rendering.

What You'll Build

You're going to build a working JavaScript to-do list with two core features:

  1. Add a task — type something into a text box, click a button, and it appears in the list.
  2. Remove a task — click a delete button next to any task, and that task disappears.

Here's what the finished behavior looks like. After typing "Buy groceries" and clicking Add, then typing "Walk the dog" and clicking Add again:

- Buy groceries
- Walk the dog

Click the remove button next to "Buy groceries," and the list becomes:

- Walk the dog

That's the whole app. No frameworks, no build tools, no libraries. Just HTML, CSS, and plain JavaScript.

If you've already built a click counter, you know the basic rhythm: listen for a click, run a function, update the page. This project keeps that rhythm but adds a new challenge—managing many items at once instead of a single number. That's where arrays come in.

Set Up the Page

Create a new folder for this beginner JavaScript project and add a file called index.html. Start with this structure:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>My To-Do List</title>
</head>
<body>
  <h1>My To-Do List</h1>

  <input type="text" id="taskInput" placeholder="Enter a task...">
  <button id="addButton">Add Task</button>

  <ul id="taskList"></ul>

  <script src="script.js"></script>
</body>
</html>

Three elements matter here:

  • The <input> is where the user types a task. It has an id of taskInput so JavaScript can read what's inside it.
  • The <button> triggers the add action when clicked.
  • The <ul> (unordered list) is where tasks will appear. It starts empty. Its id of taskList is how JavaScript finds it and fills it with items.

Notice the <script> tag sits at the bottom of the body, after all the HTML elements. That placement matters: JavaScript runs top to bottom, and if the script loads before the input and list exist, it can't find them. Putting the script last guarantees the page elements are already there.

You can add a little CSS to make it look nicer, but it's completely optional. Plain HTML works fine for learning the JavaScript mechanics. The styling is just window dressing.

Store Tasks in an Array

Here's the core problem: when a user adds a task, where does it live? The input box clears after adding. The page doesn't remember anything on its own. You need a place to store the tasks between actions.

That place is an array—a variable that holds an ordered list of items. Think of it as the app's memory for tasks.

Create a file called script.js and start with this:

let tasks = [];

That's an empty array, ready to hold tasks. The [] is the array itself, and tasks is the name you'll use to refer to it.

To add an item to an array, use the push() method:

let tasks = [];
tasks.push("Buy groceries");
tasks.push("Walk the dog");
console.log(tasks);
["Buy groceries", "Walk the dog"]

push() adds one item to the end of the list. That's all it does—and it's exactly what you need when a user adds a new task.

Knowledge check

Check your understanding

Answer this question before you continue.

What role does the `tasks` array play in this to-do list?
Single Choice

Focus: Explain how the tasks array stores the app's current tasks.

Add a Task

Now let's wire up the add flow. When the user types a task and clicks the button, four things need to happen:

  1. Read what's in the input box.
  2. Check that it's not empty.
  3. Add it to the tasks array.
  4. Clear the input box.

Here's the function. Add this below your let tasks = []; line, and keep only that one array declaration:

function addTask() {
  const input = document.getElementById("taskInput");
  const text = input.value.trim();

  if (text === "") {
    return;
  }

  tasks.push(text);
  input.value = "";
  displayTasks();
}

Let's walk through it line by line.

document.getElementById("taskInput") finds the input box on the page. Then .value reads whatever text the user typed into it.

The .trim() part removes extra spaces from the beginning and end. That way, if the user accidentally types a space before their task, it doesn't count as part of the task text.

The if statement is a guard against empty input. If the user clicks Add without typing anything, text will be an empty string (""), and the function stops right there with return. Without this guard, you'd end up with blank items cluttering your list.

If there is real text, tasks.push(text) adds it to the array. Then input.value = "" clears the input box so it's ready for the next task.

Finally, displayTasks() is called—but you haven't written that function yet. That's the next step, and it's the most important part of the whole app.

To connect the button to this function, add an event listener at the bottom of your script:

document.getElementById("addButton").addEventListener("click", addTask);

This says: when the Add button is clicked, run the addTask function. Same event-listener pattern you used in the click counter.

Knowledge check

Check your understanding

Answer this question before you continue.

A learner wants Add to do nothing when the input contains only spaces. Which replacement for the condition correctly supports that behavior?
Debugging

Focus: Use an empty-input guard to prevent blank tasks from being added.

const text = input.value.trim();

if ( /* condition */ ) {
  return;
}

Display the List

Flowchart showing Add or Remove action leading to an updated tasks array, then displayTasks clearing and rebuilding the visible list, which reflects the array.
The array is the source of truth: after every add or remove, displayTasks rebuilds the page to match it.

Here's the key idea that makes this app work: every time the array changes, rebuild the entire visible list from scratch.

This is called re-rendering. Instead of trying to surgically insert one new item into the page, you wipe the list clean and rebuild it from the array. The array is the source of truth; the page is just a reflection of it.

Add this function below addTask:

function displayTasks() {
  const list = document.getElementById("taskList");
  list.innerHTML = "";

  for (let i = 0; i < tasks.length; i++) {
    const li = document.createElement("li");
    li.textContent = tasks[i];
    list.appendChild(li);
  }
}

Here's what each part does:

  • list.innerHTML = "" clears the list. Any items currently shown are removed.
  • The for loop visits every task in the array, one at a time.
  • For each task, document.createElement("li") creates a new list item element.
  • li.textContent = tasks[i] sets that item's text to the current task.
  • list.appendChild(li) adds the item to the visible list on the page.

The loop is the heart of it: visit each task and turn it into a line on the page. If the array has three tasks, the loop runs three times and creates three list items.

After adding two tasks, the rendered page looks like:

- Buy groceries
- Walk the dog

This re-render approach might feel wasteful—why rebuild everything when you only added one item? But for a list this size, it's fast, simple, and nearly impossible to get wrong. The page always matches the array, because the page is built from the array every single time.

Knowledge check

Check your understanding

Answer this question before you continue.

Which statement best describes the re-rendering approach used in the tutorial?
Misconception Check

Focus: Explain why the list is rebuilt from the tasks array after data changes.

Remove a Task

Adding tasks is only half the app. Now you need to let users remove them.

The tricky part: when the user clicks a remove button, how does JavaScript know which task to remove?

The answer is position. Every item in an array has an index—a number representing its position, starting at 0. The first task is at index 0, the second at index 1, and so on.

To remove an item at a specific position, use splice():

tasks.splice(index, 1);

splice() takes two arguments: where to start, and how many items to remove. So splice(1, 1) means "start at position 1 and remove one item." That removes the second task.

Now replace your current displayTasks() function with this version, which gives each list item its own remove button:

function displayTasks() {
  const list = document.getElementById("taskList");
  list.innerHTML = "";

  for (let i = 0; i < tasks.length; i++) {
    const li = document.createElement("li");
    li.textContent = tasks[i];

    const removeButton = document.createElement("button");
    removeButton.textContent = "Remove";
    removeButton.addEventListener("click", function () {
      removeTask(i);
    });

    li.appendChild(removeButton);
    list.appendChild(li);
  }
}

Each task now gets a Remove button next to it. When that button is clicked, it calls removeTask(i), passing the index of that specific task.

Here's the mechanism worth understanding: each time the loop runs, it creates a fresh button, and that button's click handler remembers the current value of i for that pass through the loop. The first button created remembers 0, the second remembers 1, and so on. Then, when you remove a task and re-render, the whole list is rebuilt from the updated array—so every button gets a fresh, correct index.

Now write the removeTask function. Add this below displayTasks:

function removeTask(index) {
  tasks.splice(index, 1);
  displayTasks();
}

Two lines. splice(index, 1) removes the task at that position from the array, and displayTasks() re-renders the list to match.

If the list shows "Buy groceries" and "Walk the dog," and the user clicks Remove next to "Buy groceries" (index 0), the array becomes ["Walk the dog"], and the page re-renders to show only that task.

This is why removing by position matters. If you tried to remove by matching the task text instead, you'd run into trouble when two tasks have the same text—say, "Call mom" and "Call mom." Which one should be removed? Position is unambiguous. Each task has its own index, so each remove button knows exactly which task it belongs to.

Knowledge check

Check your understanding

Answer this question before you continue.

After this code runs, what is the value of `tasks`?
Output Prediction

Focus: Predict which task remains after removing an item by its array index.

let tasks = ["Buy groceries", "Walk the dog", "Read"];
tasks.splice(1, 1);

Test Your Work

Once you've assembled all the pieces, open index.html in your browser and run through these checks:

  1. Type "Buy groceries" and click Add. The task appears in the list.
  2. Click Add with an empty input. Nothing happens—no blank items appear.
  3. Add a second task, then click Remove next to the first one. Only the second task remains.

If any of those steps fail, check the common mistakes below.

Common Beginner Mistakes

Every beginner hits these. Here's what they look like and how to fix them fast.

Mistake 1: Forgetting to re-render

Symptom: You click Add, the input clears, but nothing appears in the list. Or you click Remove, and the task stays on the page.

Cause: You updated the array but never called displayTasks(). The array changed, but the page didn't.

Fix: Make sure displayTasks() is the last line in both addTask() and removeTask(). The page only updates when you tell it to.

Mistake 2: Removing by text instead of position

Symptom: Removing a task deletes the wrong one, or removes all tasks with the same text.

Cause: You tried to find and remove a task by matching its text, like tasks.indexOf("Buy groceries"). If two tasks share the same text, this breaks.

Fix: Always remove by index. Each remove button should know its own position in the array, which is why the loop passes i to removeTask(i).

Mistake 3: Script placed before the HTML elements

Symptom: You get an error like Cannot read properties of null when the page loads.

Cause: Your <script> tag is in the <head> or at the top of the <body>. The script runs before the input and list elements exist, so document.getElementById() can't find them.

Fix: Move the <script> tag to the bottom of the <body>, after all your HTML elements. The elements will exist by the time the script runs.

Mistake 4: Duplicate code from copying snippets

Symptom: You get an error like Identifier 'tasks' has already been declared, or your remove buttons don't appear.

Cause: You pasted every code block into script.js without replacing the earlier versions. Remember: you only need one let tasks = []; line, and the second displayTasks() version replaces the first.

Fix: Compare your file against the structure below. You should have exactly one array declaration, one addTask, one displayTasks, one removeTask, and one event listener.

Your Complete Script

Here's what your full script.js should look like when assembled correctly:

let tasks = [];

function addTask() {
  const input = document.getElementById("taskInput");
  const text = input.value.trim();

  if (text === "") {
    return;
  }

  tasks.push(text);
  input.value = "";
  displayTasks();
}

function displayTasks() {
  const list = document.getElementById("taskList");
  list.innerHTML = "";

  for (let i = 0; i < tasks.length; i++) {
    const li = document.createElement("li");
    li.textContent = tasks[i];

    const removeButton = document.createElement("button");
    removeButton.textContent = "Remove";
    removeButton.addEventListener("click", function () {
      removeTask(i);
    });

    li.appendChild(removeButton);
    list.appendChild(li);
  }
}

function removeTask(index) {
  tasks.splice(index, 1);
  displayTasks();
}

document.getElementById("addButton").addEventListener("click", addTask);

If your file matches this, and your HTML has the script tag at the bottom of the body, the app should work.

Practice and Next Steps

You've built a working JavaScript to-do list. Before moving on, try these two challenges to make sure the pattern sticks:

Challenge 1: Add a "Clear All" button. Add a button that empties the entire list. You'll need to set tasks = [] and then call displayTasks(). That's it—two lines plus a button in your HTML.

Challenge 2: Block duplicate tasks. Before pushing a new task, check if it's already in the array. The includes() method can help: if (tasks.includes(text)) { return; }. This prevents the same task from being added twice.

When you're ready for the next step, the natural one is saving tasks so they survive a page refresh. Right now, refreshing the page wipes everything—the array lives only in memory. The browser's localStorage lets you save the array as text and load it back when the page opens. That's the feature that turns this from a demo into something you might actually use.

But here's what to notice first: the pattern you just built—events, arrays, and re-rendering—is the same core loop behind much larger apps. Big applications use more sophisticated tools to manage that loop, but the underlying idea is identical: user does something, data changes, the page updates to match. You've built that loop from scratch. Everything from here is a refinement of it.

Knowledge check

Final check

Finish the article by checking the ideas you just learned.

Starting with an empty app, what does the list display after the user adds `"Buy groceries"` and then `"Walk the dog"`?
Question 1 of 2Output Prediction

Focus: Predict the task array and displayed list after adding valid tasks.

What happens to the tasks in this tutorial when the page is refreshed?
Question 2 of 2Misconception Check

Focus: Recognize that the tutorial's in-memory array does not survive a page refresh.

References

  1. JavaScript To-Do List Projectwww.w3schools.com
7sources checked
7source domains
5searches run

Research updated Sep 6, 2026

Keep learning

Related tutorials

Continue with nearby JavaScript topics and beginner-friendly explanations.

Dramatic aerial view of Panama City skyline during sunset showcasing modern skyscrapers.
beginner
10 min read

Build a Click Counter

There's a moment in learning JavaScript when things stop being abstract. You've studied variables and functions. You've followed along with examples. But…

Read tutorial
Close-up of a tropical flower with vibrant red and yellow petals in vivid detail.
beginner
13 min read

Build a JavaScript Quiz App

You've learned arrays, conditionals, click events, and DOM updates as separate lessons. Now it's time to see them work together. A quiz app is the perfect…

Read tutorial