Simple Todo List
What you'll build
A page with an input, an Add button, and a list. Tasks can be checked off and removed. The list is re-rendered from an array on every change.
Goals
- Keep todo state in an array of objects
- Render the list from state on every change
- Add, toggle, and delete items via DOM events
- Use unique ids to identify items in the DOM
Prerequisites
- Arrays and array methods (push, filter, map)
- Objects and property access
- DOM selection and event listeners
Steps
- 1
Set up state and HTML
Create an input, an Add button, and an empty ul. In JS, declare a
todosarray and anextIdcounter. State drives everything; the DOM is just a view of it.Code<input id="task-input" placeholder="Add a task"> <button id="add-btn">Add</button> <ul id="list"></ul> <script> let todos = []; let nextId = 1; </script>Expected result
An input, a button, and an empty list area appear.
- 2
Write the render function
Write
render()that clears the ul and rebuilds it from the todos array. Each li gets a checkbox, the title, and a delete button, plus a data-id attribute to identify it.Codeconst listEl = document.getElementById("list"); function render() { listEl.innerHTML = ""; todos.forEach(t => { const li = document.createElement("li"); li.innerHTML = ` <input type="checkbox" ${t.done ? "checked" : ""} data-id="${t.id}"> <span style="text-decoration:${t.done ? "line-through" : "none"}">${t.title}</span> <button data-id="${t.id}" class="del">x</button> `; listEl.appendChild(li); }); }Expected result
Calling render() draws the current todos; an empty array shows nothing.
Hint
Re-rendering on every change is simple and correct. Performance optimizations come later.
- 3
Wire the Add button
On Add click, read the input value, push a new task object to todos, increment nextId, clear the input, and call render(). Empty input is ignored.
Codeconst inputEl = document.getElementById("task-input"); const addBtn = document.getElementById("add-btn"); addBtn.addEventListener("click", () => { const title = inputEl.value.trim(); if (!title) return; todos.push({ id: nextId++, title, done: false }); inputEl.value = ""; render(); });Expected result
Typing a task and clicking Add adds it to the list and clears the input.
- 4
Handle toggle and delete with delegation
Add one click listener on the ul. If the clicked element is a checkbox, toggle that task's done flag. If it is a delete button, filter the task out. Then re-render. Event delegation handles all current and future items with one listener.
CodelistEl.addEventListener("click", (e) => { const id = Number(e.target.dataset.id); if (e.target.type === "checkbox") { const t = todos.find(t => t.id === id); if (t) t.done = e.target.checked; } else if (e.target.classList.contains("del")) { todos = todos.filter(t => t.id !== id); } render(); });Expected result
Checking a box strikes through the task; clicking x removes it.
Try it yourself
<input id="task-input" placeholder="Add a task">
<button id="add-btn">Add</button>
<ul id="list"></ul>
<script>
let todos = [];
let nextId = 1;
// TODO: render(), Add listener, ul click delegation
</script><input id="task-input" placeholder="Add a task">
<button id="add-btn">Add</button>
<ul id="list"></ul>
<script>
let todos = [];
let nextId = 1;
const listEl = document.getElementById("list");
const inputEl = document.getElementById("task-input");
const addBtn = document.getElementById("add-btn");
function render() {
listEl.innerHTML = "";
todos.forEach(t => {
const li = document.createElement("li");
li.innerHTML = `
<input type="checkbox" ${t.done ? "checked" : ""} data-id="${t.id}">
<span style="text-decoration:${t.done ? "line-through" : "none"}">${t.title}</span>
<button data-id="${t.id}" class="del">x</button>
`;
listEl.appendChild(li);
});
}
addBtn.addEventListener("click", () => {
const title = inputEl.value.trim();
if (!title) return;
todos.push({ id: nextId++, title, done: false });
inputEl.value = "";
render();
});
listEl.addEventListener("click", (e) => {
const id = Number(e.target.dataset.id);
if (e.target.type === "checkbox") {
const t = todos.find(t => t.id === id);
if (t) t.done = e.target.checked;
} else if (e.target.classList.contains("del")) {
todos = todos.filter(t => t.id !== id);
}
render();
});
</script>What you learned
You built a complete CRUD todo list using state-driven rendering and event delegation — two patterns that scale to real applications. The same architecture runs behind most interactive UIs.
Related Resources
Related Lessons
Related Practice
- Array Methods MCQ
Quiz on the array methods used to manage the list.
- DOM Events MCQ
Event delegation and dataset access used in the click handler.
Related Tools
- JSON Formatter
Inspect the todos array as JSON — exactly how it would be persisted.
Related Cheatsheets
- JavaScript Cheatsheet
Array, object, DOM, and event syntax reference.
Related Reference
- JavaScript Array Methods
Complete array method reference.
- JavaScript Object
Object methods and properties.
Related Errors
- Cannot Read Properties of Undefined
Undefined element access.
- Reference Error Not Defined
Using undefined variables.
Related Glossary
- Array
Ordered collections of values.
- Object
Key-value data structures.
- DOM
Document Object Model basics.
- Event Listener
Handling events in JS.
Related Glossary
A beginner-friendly glossary explains jargon in plain English — variables, functions, DOM, Promise, and more.
- Array
Ordered collections of values.
- Object
Key-value data structures.
- DOM
Document Object Model basics.
- Event Listener
Handling events in JS.
Related Errors
Plain-English explanations of common errors — what they mean, why they happen, and how to fix them.
- Cannot Read Properties of Undefined
Undefined element access.
- Reference Error Not Defined
Using undefined variables.