Add todos with JavaScript
Lesson 2 of 4 — turn the static list into one driven by data.
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).
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.
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 →
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) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c], );} render();<!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>* { 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; }Checkpoint
Make new todos appear at the TOP of the list instead of the bottom.
Concepts covered · 3
More like this
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.
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.
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 inNo pinned answers yet for this tutorial.