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 dueDate field and highlight overdue items
  • Export/import — download todos as JSON for backup