On this page
Todo List App
Build a fully functional todo list in plain JavaScript — no frameworks required. You will practice DOM APIs, event handling, and browser storage.
Requirements
- Modern browser and a text editor (VS Code recommended)
- Basic HTML, CSS, and JavaScript knowledge
- Familiarity with DOM and localStorage
Features
- Add new todos via a text input and button
- Mark todos as complete (strikethrough + checkbox)
- Delete individual todos
- Persist todos across page reloads using
localStorage
Step 1: Project Setup
Create a folder todo-app with three files:
todo-app/
├── index.html
├── style.css
└── app.js
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Todo List</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<h1>My Todos</h1>
<form id="todo-form">
<input id="todo-input" type="text" placeholder="Add a task..." required>
<button type="submit">Add</button>
</form>
<ul id="todo-list"></ul>
</div>
<script src="app.js"></script>
</body>
</html>
Step 2: Style the Layout
style.css
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; background: #f5f5f5; padding: 2rem; }
.container { max-width: 480px; margin: 0 auto; background: #fff; padding: 1.5rem; border-radius: 8px; }
#todo-form { display: flex; gap: 0.5rem; margin: 1rem 0; }
#todo-input { flex: 1; padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px; }
.todo-item { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0; border-bottom: 1px solid #eee; }
.todo-item.completed span { text-decoration: line-through; color: #999; }
.delete-btn { margin-left: auto; background: #e74c3c; color: #fff; border: none; padding: 0.25rem 0.5rem; border-radius: 4px; cursor: pointer; }
Step 3: Manage State with localStorage
app.js — start with data helpers:
const STORAGE_KEY = 'todos';
function loadTodos() {
const data = localStorage.getItem(STORAGE_KEY);
return data ? JSON.parse(data) : [];
}
function saveTodos(todos) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
}
let todos = loadTodos();
Each todo is an object: { id, text, completed }. Use Date.now() for unique IDs.
Step 4: Render the List
const listEl = document.getElementById('todo-list');
function render() {
listEl.innerHTML = '';
todos.forEach(todo => {
const li = document.createElement('li');
li.className = `todo-item${todo.completed ? ' completed' : ''}`;
li.dataset.id = todo.id;
li.innerHTML = `
<input type="checkbox" ${todo.completed ? 'checked' : ''}>
<span>${todo.text}</span>
<button class="delete-btn">Delete</button>
`;
listEl.appendChild(li);
});
}
render();
Step 5: Handle Events
Use event delegation on the list so one listener handles all items:
document.getElementById('todo-form').addEventListener('submit', e => {
e.preventDefault();
const input = document.getElementById('todo-input');
const text = input.value.trim();
if (!text) return;
todos.push({ id: Date.now(), text, completed: false });
saveTodos(todos);
input.value = '';
render();
});
listEl.addEventListener('click', e => {
const li = e.target.closest('.todo-item');
if (!li) return;
const id = Number(li.dataset.id);
if (e.target.type === 'checkbox') {
todos = todos.map(t => t.id === id ? { ...t, completed: !t.completed } : t);
} else if (e.target.classList.contains('delete-btn')) {
todos = todos.filter(t => t.id !== id);
}
saveTodos(todos);
render();
});
Open index.html in your browser and verify todos survive a refresh.
Step 6: Polish
- Add an empty-state message when the list has no items
- Show a count: “3 items remaining”
- Clear completed todos with a single button
Extension Ideas
- Filter tabs — All / Active / Completed views
- Edit in place — double-click a todo to rename it
- Drag-and-drop reordering — use the HTML5 Drag API
- Due dates — store an optional
dueDatefield and highlight overdue items - Export/import — download todos as JSON for backup