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

P
Reference·beginner·16 min

Persist & filter

Save the list to localStorage so it is still there tomorrow, and wire the All / Active / Done filters. This is the lesson that turns a toy into something you would actually use.

W
Reference·intermediate·16 min

Write the spec the model can hit

A prompt is a specification. The difference between a junior and a senior prompt is the constraints a senior names that a junior leaves to chance. This lesson turns your architecture sketch into a prompt that produces reviewable code.

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.