Build Project

Learn by Building

Home › Build Projects › Web Apps › To-Do App

Web App · Beginner

Build a To-Do App

Vanilla HTML, CSS, and JavaScript. Persists with localStorage. No frameworks, no build step, no dependencies.

⏱️ 45 minutes · 📄 3 files · 🎯 Beginner

What you'll build: A to-do app where you can add tasks, mark them complete, and delete them. Everything saves automatically to your browser — reload the page, and your tasks are still there.

What you'll learn: DOM manipulation, event handling, state management, rendering from data, and localStorage. These four skills power every modern web app.

🎬 Live Preview
0 tasks

The three files

Everything lives in one folder. No build step.

todo-app/
├── index.html      ← Structure
├── style.css       ← Appearance
└── app.js          ← Behavior
1

index.html — the structure

The HTML is deliberately minimal. Three elements matter: an input, an add button, and a list container. The JavaScript will fill the list.

index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>To-Do App</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <div class="app">
    <h1>My Tasks</h1>
    <div class="input-row">
      <input type="text" id="taskInput" placeholder="Add a task..." />
      <button id="addBtn">Add</button>
    </div>
    <div id="taskList"></div>
    <div id="counter"></div>
  </div>
  <script src="app.js"></script>
</body>
</html>

Why so little HTML? In a real app, JavaScript builds most of the UI from data. You render the structure once, then update it as state changes.

2

style.css — the appearance

Dark mode by default. Clean spacing. The list items animate on hover. Nothing fancy — just tasteful.

style.css
* { margin: 0; padding: 0; box-sizing: border-box; }

body {
  font-family: system-ui, sans-serif;
  background: #0f172a;
  color: #e2e8f0;
  min-height: 100vh;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 3rem 1rem;
}

.app {
  width: 100%;
  max-width: 500px;
  background: #1e293b;
  padding: 1.5rem;
  border-radius: 1rem;
}

h1 {
  font-size: 1.35rem;
  margin-bottom: 1.25rem;
}

.input-row {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 1rem;
}

input[type="text"] {
  flex: 1;
  background: #0f172a;
  border: 1px solid #334155;
  color: #e2e8f0;
  padding: 0.65rem 0.85rem;
  border-radius: 0.5rem;
  font-size: 0.9rem;
  font-family: inherit;
}

input[type="text"]:focus {
  outline: none;
  border-color: #3b82f6;
}

#addBtn {
  background: #3b82f6;
  color: #fff;
  border: none;
  padding: 0 1rem;
  border-radius: 0.5rem;
  font-weight: 600;
  cursor: pointer;
}

#addBtn:hover { background: #2563eb; }

.task {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.65rem 0.85rem;
  background: #0f172a;
  border-radius: 0.5rem;
  margin-bottom: 0.4rem;
}

.task.done .task-text {
  text-decoration: line-through;
  color: #64748b;
}

.task-text { flex: 1; font-size: 0.9rem; }

.task input[type="checkbox"] {
  width: 18px;
  height: 18px;
  cursor: pointer;
  accent-color: #22c55e;
}

.delete-btn {
  background: none;
  border: none;
  color: #64748b;
  cursor: pointer;
  font-size: 1.1rem;
  padding: 2px 6px;
  border-radius: 0.375rem;
}

.delete-btn:hover {
  color: #ef4444;
  background: rgba(239, 68, 68, 0.1);
}

#counter {
  color: #64748b;
  font-size: 0.75rem;
  text-align: right;
  margin-top: 0.5rem;
}

.empty {
  color: #64748b;
  font-size: 0.85rem;
  text-align: center;
  padding: 1.5rem 0;
}
3

app.js — the behavior

This is where everything happens. Three functions: render(), addTask(), and a small save/load pair for localStorage.

app.js
// State — the single source of truth
let tasks = [];

// Grab DOM elements once
const input = document.getElementById('taskInput');
const addBtn = document.getElementById('addBtn');
const list = document.getElementById('taskList');
const counter = document.getElementById('counter');

// ============ STORAGE ============
function save() {
  localStorage.setItem('tasks', JSON.stringify(tasks));
}

function load() {
  const stored = localStorage.getItem('tasks');
  if (stored) tasks = JSON.parse(stored);
}

// ============ RENDER ============
function render() {
  list.innerHTML = '';

  if (tasks.length === 0) {
    list.innerHTML = '<div class="empty">No tasks yet. Add one above.</div>';
  } else {
    tasks.forEach(task => {
      const row = document.createElement('div');
      row.className = 'task' + (task.done ? ' done' : '');

      const check = document.createElement('input');
      check.type = 'checkbox';
      check.checked = task.done;
      check.addEventListener('change', () => {
        task.done = !task.done;
        save();
        render();
      });

      const text = document.createElement('span');
      text.className = 'task-text';
      text.textContent = task.text;

      const del = document.createElement('button');
      del.className = 'delete-btn';
      del.textContent = '✕';
      del.addEventListener('click', () => {
        tasks = tasks.filter(t => t.id !== task.id);
        save();
        render();
      });

      row.append(check, text, del);
      list.appendChild(row);
    });
  }

  const remaining = tasks.filter(t => !t.done).length;
  counter.textContent = `${remaining} of ${tasks.length} tasks remaining`;
}

// ============ ADD ============
function addTask() {
  const text = input.value.trim();
  if (!text) return;

  tasks.push({
    id: Date.now(),
    text,
    done: false
  });

  input.value = '';
  save();
  render();
}

// ============ EVENTS ============
addBtn.addEventListener('click', addTask);
input.addEventListener('keydown', e => {
  if (e.key === 'Enter') addTask();
});

// ============ INIT ============
load();
render();

How the code works

The pattern behind this app is state → render → save. Every action follows the same three steps:

1. The state is a single array

tasks holds every task as an object: {id, text, done}. Nothing else stores task data — the array is the truth.

2. The UI is derived from state

Every time state changes, render() clears the list and rebuilds it from tasks. No partial updates, no bugs from stale DOM.

3. State is persisted after every change

save() writes the array to localStorage as JSON. On page load, load() reads it back. That's the entire persistence layer.

⚠️ Common problems

Tasks disappear on reload

You forgot to call save() after mutating tasks, or you didn't call load() on startup. Check that localStorage.setItem('tasks', ...) is present in save().

Cannot read property 'appendChild' of null

The script runs before the DOM is ready. Either move <script> to the end of <body>, or wrap the init code in DOMContentLoaded.

Checkbox doesn't toggle

You probably mutated task.done without calling render(). The DOM won't update on its own — you have to re-render.

Deleting removes the wrong task

Classic closure bug — you likely used a loop index instead of the task ID. This tutorial uses task.id specifically to avoid that issue.

🎯 What to add next

❓ Frequently asked questions

What will I learn building a to-do app?

You'll learn DOM manipulation, event handling, state management with a JavaScript array, rendering UI from data, and persisting state with localStorage. These are the four skills behind every modern web app.

How long does this build take?

About 45 minutes if you copy-paste and run it. About 2 hours if you type every line and follow each explanation. Both are valid — the second sticks better.

Do I need to install anything?

No. This build runs entirely in your browser. You need a text editor and a browser. No Node.js, no npm, no build tools.

Why use localStorage instead of a database?

localStorage is perfect for a first project because it requires zero setup. The data persists between page reloads and lives in the user's browser. Once you're comfortable, you can add a backend later.

How do I deploy this app?

Drag the folder to Netlify Drop or push it to GitHub and enable GitHub Pages. Both are free and give you a live URL in under 5 minutes.

🔗 Related

← All Build Projects