What you will build and what you need

A to-do list is a useful practice project because it combines a form, buttons, page updates, and saved data. In this version, you can add a task, mark it complete, or remove it. The list is stored in localStorage, so it can appear again when you reload the page in the same browser.

You need a text editor, a modern browser with localStorage support, and Python 3 to serve the file locally. No JavaScript library or package installation is required. The example uses standard HTML, CSS, and browser JavaScript APIs rather than features tied to a particular library version. localStorage belongs to the browser and site you use; it is not a shared account or a backup service.

Create the page

Make a folder for the project and save the following as index.html. The form collects a task, while the list is initially empty because JavaScript fills it from saved data. The status line reports when a browser cannot save a change.

<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>My to-do list</title> <style> body { font: 1rem/1.5 system-ui, sans-serif; max-width: 38rem; margin: 2rem auto; padding: 0 1rem; } form, li { display: flex; gap: .5rem; align-items: center; } input[type="text"] { flex: 1; min-width: 0; } li { justify-content: space-between; margin: .5rem 0; } li label { overflow-wrap: anywhere; } .done { text-decoration: line-through; } </style> </head> <body> <h1>My to-do list</h1> <form id="task-form"> <label for="task-input">New task</label> <input id="task-input" type="text" required> <button type="submit">Add</button> </form> <p id="status" role="status"></p> <ul id="task-list"></ul> <script> const key = "practice-todo-tasks"; const form = document.querySelector("#task-form"); const input = document.querySelector("#task-input"); const list = document.querySelector("#task-list"); const status = document.querySelector("#status"); function loadTasks() { try { const saved = JSON.parse(localStorage.getItem(key) || "[]"); if (!Array.isArray(saved)) return []; return saved.filter(task => task && typeof task.text === "string" && typeof task.done === "boolean" ); } catch { status.textContent = "Saved tasks could not be loaded."; return []; } } let tasks = loadTasks(); function saveTasks() { try { localStorage.setItem(key, JSON.stringify(tasks)); status.textContent = ""; } catch { status.textContent = "Tasks changed here, but could not be saved."; } } function render() { list.replaceChildren(); tasks.forEach(task => { const item = document.createElement("li"); const label = document.createElement("label"); const checkbox = document.createElement("input"); const text = document.createElement("span"); const remove = document.createElement("button"); checkbox.type = "checkbox"; checkbox.checked = task.done; text.textContent = task.text; text.className = task.done ? "done" : ""; remove.type = "button"; remove.textContent = "Remove"; remove.setAttribute("aria-label", `Remove ${task.text}`); checkbox.addEventListener("change", () => { task.done = checkbox.checked; saveTasks(); render(); }); remove.addEventListener("click", () => { tasks = tasks.filter(entry => entry !== task); saveTasks(); render(); }); label.append(checkbox, " ", text); item.append(label, remove); list.append(item); }); } form.addEventListener("submit", event => { event.preventDefault(); const text = input.value.trim(); if (!text) return; tasks.push({ text, done: false }); saveTasks(); render(); input.value = ""; input.focus(); }); render(); </script> </body> </html>

How the saved list works

The tasks variable holds an array of objects. Each object has text and a done value. When you add, check, or remove a task, the code changes that array and calls saveTasks(). JSON.stringify turns the array into a string, which localStorage can store under the project’s key. On page load, JSON.parse turns that string back into an array. If there is no saved value, the list starts empty.

The render() function rebuilds the visible list from the current array. It uses textContent for task names, so text entered by a user is displayed as text rather than interpreted as HTML. loadTasks() also checks the basic shape of saved items before using them. These choices make the example more dependable without adding a framework.

Run and check your project

Open a terminal in the project folder and run python3 -m http.server 8000. If your system starts Python 3 with py, use py -3 -m http.server 8000 instead. Visit http://127.0.0.1:8000 in your browser. Serving the file locally avoids the differences browsers may have when using localStorage with a file opened directly from your computer.

Add two tasks, complete one, and remove the other. Reload the page: the remaining task and its completed state should still appear. Try submitting only spaces; no task should be added. To start over, remove tasks with the buttons, or clear this site’s storage using your browser’s developer tools.

localStorage is suitable for small practice projects, but browser settings, private browsing, or storage limits can prevent changes from being saved. This example shows a message if saving fails; changes may still be visible until you reload. Tasks are stored only in that browser’s storage for this local address, so do not use this list for information you cannot afford to lose.

Back to category ↗