Mini Project: Interactive To-Do List
This Chapter Introduces No New Concepts
Everything here uses what you already have: selecting elements, creating elements, listening for events, and delegation. This lesson builds all of it into one working, fully styled page, one small piece at a time.
💡 Why this matters: Real interactive pages combine several small techniques at once. This is where “I know the DOM and I know events” turns into “I can build something.”
What You’re Building
A to-do list where a user can type a task and add it, click a task to mark it complete, and click a delete button to remove it. No page reload, no server, everything happens live in the DOM.
The Starter Page
The design is already done. Save this as index.html and open it in a browser, everything below is about filling in the <script> tag at the bottom.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Modern Todo List</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: Arial, Helvetica, sans-serif;
}
body {
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background: linear-gradient(135deg, #6a11cb, #2575fc);
padding: 20px;
}
.container {
width: 100%;
max-width: 420px;
background: rgba(255, 255, 255, .15);
backdrop-filter: blur(15px);
border: 1px solid rgba(255, 255, 255, .2);
border-radius: 20px;
padding: 25px;
box-shadow: 0 15px 35px rgba(0, 0, 0, .3);
}
h1 {
text-align: center;
color: #fff;
margin-bottom: 20px;
}
.input-box {
display: flex;
gap: 10px;
}
.input-box input {
flex: 1;
padding: 14px;
border: none;
outline: none;
border-radius: 10px;
font-size: 16px;
}
.input-box button {
padding: 14px 20px;
border: none;
border-radius: 10px;
background: #00c6ff;
color: #fff;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: .3s;
}
.input-box button:hover {
background: #0096c7;
}
ul {
list-style: none;
margin-top: 20px;
}
li {
display: flex;
justify-content: space-between;
align-items: center;
background: rgba(255, 255, 255, .2);
color: #fff;
padding: 12px 15px;
border-radius: 12px;
margin-bottom: 12px;
animation: fadeIn .3s ease;
}
.task {
flex: 1;
cursor: pointer;
word-break: break-word;
}
.completed {
text-decoration: line-through;
opacity: .6;
}
.delete {
width: 38px;
height: 38px;
border: none;
border-radius: 50%;
background: transparent;
color: #ff5d5d;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: .3s;
}
.delete svg {
width: 18px;
height: 18px;
}
.delete:hover {
background: #ff4d4d;
color: #fff;
transform: rotate(10deg) scale(1.1);
box-shadow: 0 8px 15px rgba(255, 77, 77, .4);
}
.delete:active {
transform: scale(.9);
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>
</head>
<body>
<div class="container">
<h1>📝 My Todo List</h1>
<div class="input-box">
<input type="text" id="taskInput" placeholder="Enter your task...">
<button id="addBtn">Add</button>
</div>
<ul id="taskList"></ul>
</div>
<script>
// Your JavaScript goes here
</script>
</body>
</html>
#taskList starts empty. Every task gets added to it by JavaScript. Notice there’s no onclick="..." anywhere in this markup, every listener in this project gets attached the way Module 2 taught: with .addEventListener() in the <script> tag, not scattered across HTML attributes.
Step 1: Adding a Task
Start with what you need
Before writing any logic, think about what elements this feature actually touches: the input (to read what the user typed), the “Add” button (to know when to act), and the list (to add the new task to). Get references to all three first.
const input = document.getElementById("taskInput");
const addBtn = document.getElementById("addBtn");
const taskList = document.getElementById("taskList");
Decide where the logic should live
A task can be added two different ways: clicking “Add”, or pressing Enter in the input. Both need to do the exact same thing. Rather than writing that logic twice, put it in one function that both triggers can call later.
function addTask() {
// logic goes here
}
Read what the user typed
The first thing addTask() needs is the text currently in the input.
function addTask() {
const text = input.value.trim();
}
.trim() strips leading and trailing whitespace, so a task like " Write lesson " becomes "Write lesson", and a submission that’s only spaces becomes an empty string.
Handle the empty case before building anything
If there’s nothing to add, stop right here. Deciding this early, before creating any elements, means the rest of the function never has to think about the empty case again.
function addTask() {
const text = input.value.trim();
if (!text) return;
}
Build the list item
Now that you know there’s real text to work with, build what one task looks like in the DOM. Start with the outer container, an <li>.
function addTask() {
const text = input.value.trim();
if (!text) return;
const li = document.createElement("li");
}
Add the task’s text
The <li> needs a <span> inside it to hold the task text, matching the .task class already styled in the CSS above.
const task = document.createElement("span");
task.className = "task";
task.textContent = text;
Add the delete button
Each task also needs its own delete button, matching the .delete class from the CSS. The icon inside it is a small, fixed SVG string, set with .innerHTML. This is safe here (unlike the security warning back in Module 1) because you wrote this exact string yourself, it isn’t text a user typed in.
const deleteBtn = document.createElement("button");
deleteBtn.className = "delete";
deleteBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24"><path d="M9 3h6l1 2h5v2H3V5h5l1-2zm1 7h2v8h-2v-8zm4 0h2v8h-2v-8zM7 10h2v8H7v-8zm-1 11h12a2 2 0 0 0 2-2V7H4v12a2 2 0 0 0 2 2z"/></svg>`;
Put the piece together and add it to the page
The task text and delete button both need to go inside the <li>, and the <li> needs to go inside #taskList.
li.append(task, deleteBtn);
taskList.appendChild(li);
Reset the input for the next task
The last thing addTask() needs to do is clear the input and put the cursor back in it, so the user can immediately type the next task.
input.value = "";
input.focus();
Put together, addTask() looks like this:
function addTask() {
const text = input.value.trim();
if (!text) return;
const li = document.createElement("li");
const task = document.createElement("span");
task.className = "task";
task.textContent = text;
const deleteBtn = document.createElement("button");
deleteBtn.className = "delete";
deleteBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24"><path d="M9 3h6l1 2h5v2H3V5h5l1-2zm1 7h2v8h-2v-8zm4 0h2v8h-2v-8zM7 10h2v8H7v-8zm-1 11h12a2 2 0 0 0 2-2V7H4v12a2 2 0 0 0 2 2z"/></svg>`;
li.append(task, deleteBtn);
taskList.appendChild(li);
input.value = "";
input.focus();
}
Each task’s markup, once addTask() runs, looks like this on the page:
<li>
<span class="task">Write lesson</span>
<button class="delete">
<svg>...</svg>
</button>
</li>
Wire up the two triggers
addTask() exists now, but nothing calls it yet. Both the button and the Enter key should call it, and both listeners are simple because all the real work already lives inside the function.
addBtn.addEventListener("click", addTask);
input.addEventListener("keydown", (event) => {
if (event.key === "Enter") {
addTask();
}
});
Clicking “Add” and pressing Enter both call the same addTask() function, so the two ways of submitting a task can never fall out of sync with each other.
Step 2: Completing and Deleting, with Delegation
Think about what’s missing
Right now, tasks can be added, but clicking one does nothing. Clicking a task’s text should toggle it complete, and clicking its delete button should remove it. The tempting first instinct is to attach a listener inside addTask(), directly on the new task and deleteBtn elements, right when they’re created.
That would work for the moment, but recall Module 2, lesson 4: any listener attached only to elements that exist right now misses every task added afterward, unless you remembered to re-attach it inside addTask() every single time. Delegation avoids that problem entirely: attach one listener to #taskList, once, and it handles every task, current and future.
Attach one listener to the list
taskList.addEventListener("click", (event) => {
// figure out what was actually clicked
});
Check for a delete click first
Inside the listener, event.target is whatever the user actually clicked, which could be the delete button itself, or the <svg> icon inside it, or the <path> inside that. .closest(".delete") walks upward from wherever the click landed until it finds the button.
taskList.addEventListener("click", (event) => {
const deleteBtn = event.target.closest(".delete");
if (deleteBtn) {
deleteBtn.closest("li").remove();
return;
}
});
The return matters: once a delete click is handled, there’s nothing left to check for this click, no need to also test whether it was a task-text click.
Check for a task click second
If the click wasn’t on a delete button, check whether it landed on a task’s text instead.
taskList.addEventListener("click", (event) => {
const deleteBtn = event.target.closest(".delete");
if (deleteBtn) {
deleteBtn.closest("li").remove();
return;
}
const task = event.target.closest(".task");
if (task) {
task.classList.toggle("completed");
}
});
The .completed class is already defined in the stylesheet on the starter page, toggling it is all the JavaScript needs to do, the strikethrough itself is handled entirely by CSS.
💡 A note on scope: the original design this page is based on faded each task out before removing it, using
setTimeout(). Timers are covered in the next module, Asynchronous JavaScript, so this project removes a task immediately instead. Once you’ve finished that module, come back and add the fade-out yourself, it’s a small change.
Putting It Together
With both pieces in place: typing a task and clicking “Add” (or pressing Enter) adds it to the list, clicking a task’s text toggles it complete, and clicking its delete button removes it, all without a single page reload.
// Simulating a user adding two tasks, one at a time:
input.value = "Write lesson";
addBtn.click();
// list now has 1 task: "Write lesson"
input.value = "Test code";
addBtn.click();
// list now has 2 tasks
Try It
Build the full to-do list yourself, following the steps above in order:
- Add a task by clicking “Add”, then add another by pressing Enter inside the input. Confirm both appear in
#taskListin the order added. - Click a task’s text once. Confirm it gains the
completedclass and shows a strikethrough. Click it again and confirm the class is removed. - Click a task’s delete button (including clicking directly on the icon inside it). Confirm it’s removed from
#taskListand the other tasks are unaffected. - Try clicking “Add” with the input empty, or containing only spaces. Confirm nothing is added.
- Add a task, then immediately click its delete button. Confirm delegation handles it correctly, even though no listener was ever attached to that specific task.
Recap
- This project combined
createElement/append(Module 1) with event listeners and delegation (this module) into one working, styled page. - Building
addTask()step by step: read the input, bail out if empty, build the elements, attach them, reset the input, showed how to break one feature into small, ordered pieces rather than writing it all at once. - Delegation was the key piece for completing and deleting: one listener on
#taskList, attached once, correctly handles every task, including ones added long after the page first loaded.
Next module: Asynchronous JavaScript, timers, promises, async/await, and fetching real data, including that fade-out effect this project skipped.