Download the complete example ZIP
A small todo app is a useful JavaScript exercise because it connects a form, an in-memory list, browser storage and DOM rendering. This example lets you add a task, mark it complete and remove it. Refreshing the page reloads the saved list from the same browser origin. There is no account, server database, build system or npm installation.
The files deliberately keep persistence separate from rendering. That makes it easier to tell whether a bug belongs to the input rules, the saved data or the controls on screen. Use fictional task descriptions while experimenting. A browser-local list is a convenience for a learning project, not a suitable store for passwords or private business records.
Run the two-file project
Extract the download and open a terminal inside javascript-todo. With Python installed, run python -m http.server 8000 --bind 127.0.0.1 and visit http://127.0.0.1:8000/. Keep the same hostname and port when checking persistence. Opening the HTML directly from the filesystem is not the setup used for this demo.
The page contains a labelled input, a live status message and a task list. The deferred script runs after the markup is parsed. The input's maximum length improves the normal form experience, while JavaScript checks the trimmed text again before adding it. A separate hundred-task limit keeps this teaching example bounded.
index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>JavaScript Todo Demo | Toolzify</title>
<style>
:root{color-scheme:dark}body{font:17px/1.6 system-ui;background:#10191c;color:#eef8f5;max-width:680px;margin:48px auto;padding:20px}h1{line-height:1.2}form{display:flex;gap:8px;flex-wrap:wrap}input,button{font:inherit;padding:10px;border:1px solid #617975;border-radius:8px}input[type=text]{flex:1;min-width:160px;background:#172529;color:inherit}button{background:#8cf2cb;color:#10191c;cursor:pointer}li{display:flex;gap:12px;align-items:center;margin:12px 0}li span{flex:1;overflow-wrap:anywhere}ul{padding:0;list-style:none}input[type=checkbox]{width:22px;height:22px}small{color:#b4c6c0}
</style>
<script src="app.js" defer></script>
</head>
<body>
<h1>A small JavaScript todo app</h1>
<p>Add, complete and remove tasks. This demo stores this browser's list locally.</p>
<form id="taskForm">
<label for="task">New task</label>
<input id="task" name="task" type="text" maxlength="120" required>
<button>Add task</button>
</form>
<p id="status" role="status" aria-live="polite"></p>
<ul id="tasks" aria-label="Tasks"></ul>
<small>No account or sync. Use fictional tasks; do not store secrets here.</small>
</body>
</html>
app.js
'use strict';
const KEY = 'toolzify.todo.demo.v1';
const form = document.getElementById('taskForm');
const input = document.getElementById('task');
const list = document.getElementById('tasks');
const status = document.getElementById('status');
let tasks = [];
let storageFailed = false;
try {
const saved = localStorage.getItem(KEY);
if (saved !== null) {
const parsed = JSON.parse(saved);
if (!Array.isArray(parsed) || parsed.length > 100 || !parsed.every(t =>
t && typeof t.text === 'string' && t.text.length <= 120 && typeof t.done === 'boolean')) {
throw new Error('Invalid saved list');
}
tasks = parsed;
}
} catch {
storageFailed = true;
status.textContent = 'Saved tasks could not be loaded. This session works in memory; saved data will not be overwritten.';
}
function save() {
if (storageFailed) return;
try {
localStorage.setItem(KEY, JSON.stringify(tasks));
status.textContent = 'Saved in this browser.';
} catch {
storageFailed = true;
status.textContent = 'Storage is unavailable. Changes now stay in memory until this page closes.';
}
}
function render() {
list.replaceChildren();
tasks.forEach((task, index) => {
const row = document.createElement('li');
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = task.done;
checkbox.setAttribute('aria-label', `Complete ${task.text}`);
checkbox.addEventListener('change', () => {
task.done = checkbox.checked;
save();
render();
});
const label = document.createElement('span');
label.textContent = task.text;
label.style.textDecoration = task.done ? 'line-through' : 'none';
const remove = document.createElement('button');
remove.type = 'button';
remove.textContent = 'Remove';
remove.setAttribute('aria-label', `Remove ${task.text}`);
remove.addEventListener('click', () => {
tasks.splice(index, 1);
save();
render();
input.focus();
});
row.append(checkbox, label, remove);
list.appendChild(row);
});
}
form.addEventListener('submit', event => {
event.preventDefault();
const text = input.value.trim();
if (!text || text.length > 120) {
status.textContent = 'Enter a task of 1 to 120 characters.';
return;
}
if (tasks.length >= 100) {
status.textContent = 'This demo allows 100 tasks. Remove one before adding another.';
return;
}
tasks.push({ text, done: false });
save();
render();
form.reset();
input.focus();
});
render();
Follow a task through the application
Submitting the form prevents navigation, trims the input and appends an object with a text value and completion flag. The app then attempts to save the list and redraws the controls. The field is cleared and focused again, so a reader using a keyboard can continue without locating it with a mouse.
Each row receives its own checkbox and Remove button. Their accessible names include the task text. Rendering uses textContent, so text that resembles an HTML tag remains text rather than becoming executable markup. The render function rebuilds the list after each change; this is intentionally simple and is not a recommendation for very large, rapidly changing interfaces.
Handle storage failure honestly
The load path checks the stored value's shape before using it. A non-array value, an oversized list or an unexpected field causes a visible recovery message. The app continues in memory and does not overwrite the saved value. If a write fails, it also switches to memory-only behavior and tells the reader that the changes will be lost when the page closes.
Do not silently call localStorage.clear() to repair this example: that would erase unrelated keys belonging to the same origin. For a production recovery feature, export or inspect the affected key and let the user deliberately reset it. This version intentionally has no automatic repair or multi-tab synchronization. See MDN's localStorage reference for persistence, origin and exception behavior.
Check these behaviors
| Action | Expected result |
|---|---|
| Add Review PHP demo | A new unchecked row appears |
| Complete it, then refresh | The task remains checked |
| Remove the task | The row disappears and the field gets focus |
| Add text resembling an HTML element | It is shown literally |
| Submit only spaces | No task is added |
Browser verification covered adding, completion, persistence after reload, removal and literal rendering of HTML-like text. The malformed-storage branch is explained by the source; it is not claimed as a browser-tested scenario. Extend the project with an explicit export feature or filters after these basic transitions work. For a next step involving a server, try the PHP and Fetch search demo.
Comments (0)
Join the discussion