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
| Method | Job on this page |
|---|---|
| array | Holds each task as { id, text, done } |
filter | Deletes a task by keeping every other id |
| events | Add on click or Enter, toggle done, delete |
map | Flips done on the matching task |
createElement | Builds 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
renderafter 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
- Add a Clear completed button that filters out every task with
done === true. - Refuse duplicate text (case-insensitive). Show a status message instead of adding.
- Move open tasks above completed tasks in
render. Preview at /javascript/try.