TutorialsPreview
Build a Todo App

Build a Todo AppLesson 3

LearnReferencebeginner15 minBuild a Todo App

Add todos with JavaScript

Lesson 2 of 4 — turn the static list into one driven by data.

You will learn

State-then-render: keep the data in an array, redraw the DOM from it, and handle a form submit without a page reload.

Prerequisites

Lesson 1 (the structure and styles are already in place).

02

Step 215 min

Render the list from an array

GoalTyping a task and pressing Add inserts a real row — the list is now data, not markup.

We empty the hard-coded <ul> and let JavaScript own it. Hold an array of { id, title, done } objects, and write one render() that rebuilds the list from that array. On submit, push a new todo and re-render. This "single source of truth, redraw from it" loop is the whole game.

Concept · js/events
Catalog

Events

Code reacts to things that happen — a submit, a click — by registering a listener that runs when they fire.

Want the full picture? Open the reference →

project · todo-app · Add todos Open in Code
app.jsJAVASCRIPT
const list = document.getElementById('list');
const form = document.getElementById('new-todo');
const input = document.getElementById('title');
let todos = [];
form.addEventListener('submit', (e) => {
e.preventDefault();
const title = input.value.trim();
if (!title) return;
todos.push({ id: Date.now(), title, done: false });
input.value = '';
render();
});
function render() {
list.innerHTML = '';
for (const t of todos) {
const li = document.createElement('li');
li.dataset.id = t.id;
li.innerHTML =
'<span class="check"></span>' +
'<span class="title">' + esc(t.title) + '</span>' +
'<button class="remove" aria-label="Delete">\u00d7</button>';
list.appendChild(li);
}
}
function esc(s) {
return s.replace(/[&<>"]/g, (c) =>
({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[c],
);
}
render();
index.htmlHTML
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Todo</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<main class="app">
<h1>Today</h1>
<form id="new-todo">
<input id="title" name="title" placeholder="What needs doing?" autocomplete="off" />
<button type="submit">Add</button>
</form>
<ul id="list" class="list"></ul>
<footer class="bar">
<span id="count">1 left</span>
<span class="filters">
<button data-filter="all" class="active">All</button>
<button data-filter="active">Active</button>
<button data-filter="done">Done</button>
</span>
</footer>
</main>
<script src="app.js"></script>
</body>
</html>
styles.cssCSS
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: start center;
padding: 6vh 16px;
font-family: system-ui, -apple-system, sans-serif;
background: #0b0e14;
color: #e7ecf3;
}
.app { width: 100%; max-width: 460px; }
h1 { font-size: 22px; letter-spacing: -0.01em; margin: 0 0 16px; }
#new-todo { display: flex; gap: 8px; margin-bottom: 18px; }
#title {
flex: 1;
padding: 11px 14px;
border-radius: 10px;
border: 1px solid #232a36;
background: #11151c;
color: inherit;
font-size: 15px;
}
#title:focus { outline: none; border-color: #6ea8fe; }
#new-todo button {
padding: 0 18px;
border-radius: 10px;
border: 1px solid #2b3340;
background: #6ea8fe;
color: #0b0e14;
font-weight: 600;
cursor: pointer;
}
.list { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }
.list li {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 14px;
border-radius: 10px;
background: #11151c;
border: 1px solid #1c2330;
}
.check {
width: 18px;
height: 18px;
flex: none;
border-radius: 999px;
border: 1.5px solid #3a4456;
cursor: pointer;
}
.list li.done .check { background: #4ade80; border-color: #4ade80; }
.list li.done .title { color: #5b6675; text-decoration: line-through; }
.title { flex: 1; }
.remove {
border: none;
background: none;
color: #5b6675;
font-size: 20px;
line-height: 1;
cursor: pointer;
}
.remove:hover { color: #f87171; }
.bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 16px;
font-size: 13px;
color: #8b93a3;
}
.filters { display: flex; gap: 4px; }
.filters button {
border: 1px solid transparent;
background: none;
color: inherit;
padding: 4px 10px;
border-radius: 8px;
cursor: pointer;
}
.filters button.active { border-color: #2b3340; color: #e7ecf3; }
preview
Step 2 — the add-flow working. Type, press Add, watch the array render.

Checkpoint

Try it

Make new todos appear at the TOP of the list instead of the bottom.

Concepts covered · 3

More like this

R
Reference·intermediate·17 min

Review what comes back like a senior

Models produce confident, plausible, well-formatted code — which is exactly what makes their mistakes dangerous. This lesson is the review rubric: the specific failure classes to hunt for, because "it looks right" is not review.

O
Reference·advanced·18 min

Orchestrate agents without the codebase rotting

Putting it together: context, guardrails, and a verification loop that lets you run coding agents across a real codebase without it drifting into an incoherent pile. This is the senior loop at full speed.

S
Reference·beginner·12 min

Structure & style the app

Lay down the HTML skeleton and a calm dark style for a todo app. No JavaScript yet — just the shape, so the behaviour you add next has somewhere to live.

Liked this one?

Pass it on

Discussion

Be the first to ask

Your questions stay private with the team. We can pin answers to share with everyone.

Sign in to ask a question privately on this tutorial.

Sign in

No pinned answers yet for this tutorial.