JavascriptIntermediate
Todo List
Build a fully functional todo list where users can add, complete, and delete tasks.
Todo List
Problem
Build a todo list where the user can add tasks, mark them as complete, delete them, and filter between all, active, and completed tasks. Tasks should persist in localStorage so they survive a page refresh.
User adds "Buy groceries"
→ Task appears in the list — active
User adds "Write notes"
→ Second task appears
User clicks "Buy groceries"
→ Task is marked complete — strikethrough
User clicks filter "Completed"
→ Only "Buy groceries" is visible
User clicks filter "Active"
→ Only "Write notes" is visible
User refreshes the page
→ Both tasks are still thereLogic
- Load any saved tasks from localStorage on page load
- Listen for form submit to add new tasks
- Each task has an
id,text, andcompletedstate - Render the task list based on the current filter
- Clicking a task toggles its
completedstate - Clicking the delete button removes the task
- Filter buttons show all, active, or completed tasks
- Save the tasks array to localStorage after every change
Flow
HTML Structure
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Todo List</title>
<style>
body {
font-family: sans-serif;
max-width: 480px;
margin: 60px auto;
padding: 0 20px;
}
/* input row — text field and add button */
.input-row {
display: flex;
gap: 10px;
margin-bottom: 16px;
}
.input-row input {
flex: 1;
padding: 12px;
font-size: 1rem;
border: 2px solid #ccc;
border-radius: 8px;
outline: none;
box-sizing: border-box;
}
.input-row input:focus {
border-color: #555;
}
.input-row button {
padding: 12px 20px;
font-size: 1rem;
font-weight: 600;
background: #111;
color: #fff;
border: none;
border-radius: 8px;
cursor: pointer;
}
.input-row button:hover {
opacity: 0.85;
}
/* filter buttons row */
.filters {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
.filters button {
padding: 6px 16px;
border: 2px solid #ddd;
border-radius: 20px;
background: #fff;
font-size: 0.85rem;
cursor: pointer;
color: #555;
}
.filters button.active {
border-color: #111;
background: #111;
color: #fff;
}
/* task count */
#task-count {
font-size: 0.85rem;
color: #888;
margin-bottom: 12px;
}
/* task list */
#todo-list {
list-style: none;
padding: 0;
margin: 0;
}
/* each task row */
#todo-list li {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 14px;
border: 1px solid #e0e0e0;
border-radius: 8px;
margin-bottom: 8px;
cursor: pointer;
transition: background 0.15s;
}
#todo-list li:hover {
background: #f9f9f9;
}
/* completed task — strikethrough and grey */
#todo-list li.completed .task-text {
text-decoration: line-through;
color: #aaa;
}
.task-text {
flex: 1;
font-size: 1rem;
color: #333;
}
/* delete button on each task */
.delete-btn {
background: none;
border: none;
cursor: pointer;
font-size: 1.1rem;
color: #ccc;
padding: 0;
line-height: 1;
}
.delete-btn:hover {
color: #e74c3c;
}
/* empty state message */
#empty-message {
text-align: center;
color: #aaa;
font-size: 0.95rem;
padding: 24px 0;
display: none;
}
</style>
</head>
<body>
<h1>Todo List</h1>
<!-- add task form -->
<div class="input-row">
<input type="text" id="task-input" placeholder="Add a new task..." />
<button id="add-btn">Add</button>
</div>
<!-- filter buttons -->
<div class="filters">
<button class="active" data-filter="all">All</button>
<button data-filter="active">Active</button>
<button data-filter="completed">Completed</button>
</div>
<!-- task count -->
<div id="task-count"></div>
<!-- tasks rendered here -->
<ul id="todo-list"></ul>
<!-- shown when list is empty -->
<div id="empty-message">No tasks here.</div>
<script src="script.js"></script>
</body>
</html>Solution
// Step 1 — select elements
const taskInput = document.querySelector("#task-input");
const addBtn = document.querySelector("#add-btn");
const todoList = document.querySelector("#todo-list");
const taskCount = document.querySelector("#task-count");
const emptyMessage = document.querySelector("#empty-message");
const filterButtons = document.querySelectorAll(".filters button");
// Step 2 — load tasks from localStorage
// JSON.parse converts the saved string back into an array
// if nothing is saved yet — start with an empty array
let tasks = JSON.parse(localStorage.getItem("tasks")) || [];
// Step 3 — track which filter is active — "all", "active", or "completed"
let currentFilter = "all";
// Step 4 — render tasks on page load
renderTasks();
// Step 5 — add task on button click
addBtn.addEventListener("click", addTask);
// also add on Enter key
taskInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") addTask();
});
function addTask() {
const text = taskInput.value.trim();
// do nothing if input is empty
if (!text) return;
// create a new task object
const newTask = {
id: Date.now(), // unique id based on timestamp
text: text, // the task text
completed: false // new tasks start as active
};
// add to the tasks array
tasks.push(newTask);
// clear the input field
taskInput.value = "";
// save and re-render
saveAndRender();
}
// Step 6 — use event delegation on the list
// one listener handles clicks on tasks and delete buttons
todoList.addEventListener("click", (e) => {
// find the closest li — handles clicks on child elements too
const li = e.target.closest("li");
if (!li) return;
const id = Number(li.dataset.id); // read task id from data attribute
if (e.target.classList.contains("delete-btn")) {
// Step 7 — delete task — filter it out of the array
tasks = tasks.filter((task) => task.id !== id);
} else {
// Step 8 — toggle completed state
const task = tasks.find((task) => task.id === id);
if (task) task.completed = !task.completed;
}
saveAndRender();
});
// Step 9 — filter buttons
filterButtons.forEach((btn) => {
btn.addEventListener("click", () => {
// update active filter
currentFilter = btn.dataset.filter;
// update active button style
filterButtons.forEach((b) => b.classList.remove("active"));
btn.classList.add("active");
renderTasks();
});
});
function renderTasks() {
// Step 10 — filter tasks based on current filter
const filtered = tasks.filter((task) => {
if (currentFilter === "active") return !task.completed;
if (currentFilter === "completed") return task.completed;
return true; // "all" — show everything
});
// clear the list
todoList.innerHTML = "";
// Step 11 — show empty message if nothing to show
if (filtered.length === 0) {
emptyMessage.style.display = "block";
taskCount.textContent = "";
} else {
emptyMessage.style.display = "none";
// update count — only count active tasks
const activeCount = tasks.filter((t) => !t.completed).length;
taskCount.textContent = `${activeCount} task${activeCount !== 1 ? "s" : ""} remaining`;
// Step 12 — render each task as a list item
filtered.forEach((task) => {
const li = document.createElement("li");
// store task id on the element — used in the click handler
li.dataset.id = task.id;
// add completed class if task is done
if (task.completed) li.classList.add("completed");
li.innerHTML = `
<span class="task-text">${task.text}</span>
<button class="delete-btn" title="Delete task">✕</button>
`;
todoList.appendChild(li);
});
}
}
// Step 13 — save to localStorage and re-render
function saveAndRender() {
// JSON.stringify converts the array to a string for storage
localStorage.setItem("tasks", JSON.stringify(tasks));
renderTasks();
}Code Execution
Trace through adding task "Buy groceries":
| Step | Code | Result |
|---|---|---|
| Read input | taskInput.value.trim() | "Buy groceries" |
| Create task | { id: 1710000000000, text: "Buy groceries", completed: false } | new object |
| Push to array | tasks.push(newTask) | tasks.length === 1 |
| Save | localStorage.setItem("tasks", JSON.stringify(tasks)) | saved to browser |
| Render | renderTasks() | task appears in list |
Trace through clicking a task to complete it:
| Step | Code | Result |
|---|---|---|
| Click fires | e.target.closest("li") | the li element |
| Read id | li.dataset.id | "1710000000000" |
| Find task | tasks.find(t => t.id === 1710000000000) | the task object |
| Toggle | task.completed = !false | true |
| Save and render | task shows strikethrough |
Trace through filter "completed":
| Step | Code | Result |
|---|---|---|
| Filter | tasks.filter(t => t.completed) | only completed tasks |
| Render | only completed tasks shown | active tasks hidden |
Output
Add "Buy groceries" → appears in list, 1 task remaining
Add "Write notes" → appears in list, 2 tasks remaining
Click "Buy groceries" → strikethrough, 1 task remaining
Filter: Completed → only "Buy groceries" visible
Filter: Active → only "Write notes" visible
Filter: All → both tasks visible
Refresh page → both tasks still there (localStorage)