Web development tutorial: save a reading list in the browser
By Gistmax Tutorials · 2026-11-04
Build a small reading list that survives a page reload. You will handle a form submission, render user text safely and store an array in localStorage. The app stays on the current browser: it does not create an account or synchronise across devices.
1. Create the file and run a local server
Save the following as `index.html` in a new folder. In that folder, run `python -m http.server 8080` and open `http://localhost:8080`. Use a local server because storage behaviour for files opened directly from disk can differ between browsers.
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>My reading list</title>
<body>
<main>
<h1>My reading list</h1>
<form id="form">
<label for="title">Article title</label>
<input id="title" required maxlength="120">
<button>Add article</button>
</form>
<p id="status" role="status"></p>
<ul id="list"></ul>
</main>
<script>
const key = 'gistmax-tutorial-reading-list';
const list = document.querySelector('#list');
const status = document.querySelector('#status');
const input = document.querySelector('#title');
let items = [];
try {
const saved = JSON.parse(localStorage.getItem(key) || '[]');
if (Array.isArray(saved)) items = saved.filter(x => typeof x === 'string').slice(0, 100);
} catch { status.textContent = 'Saved data could not be loaded.'; }
function render() {
list.replaceChildren();
items.forEach((title, index) => {
const row = document.createElement('li');
const remove = document.createElement('button');
row.append(document.createTextNode(title + ' '));
remove.textContent = 'Remove';
remove.setAttribute('aria-label', 'Remove ' + title);
remove.onclick = () => { items.splice(index, 1); save(); render(); };
row.append(remove); list.append(row);
});
}
function save() {
try { localStorage.setItem(key, JSON.stringify(items)); status.textContent = 'Saved in this browser.'; }
catch { status.textContent = 'Changes are in memory only; browser storage is unavailable.'; }
}
document.querySelector('#form').onsubmit = event => {
event.preventDefault();
const value = input.value.trim();
if (!value) return;
if (items.length >= 100) { status.textContent = 'Remove an item before adding more.'; return; }
items.push(value); input.value = ''; save(); render(); input.focus();
};
render();
</script>
</body>
</html>2. Check the complete workflow
Add two titles, reload the page, remove one and reload again. When storage is available, only the remaining title should appear. Enter text containing angle brackets and confirm it appears as text rather than becoming an HTML element. The app creates text nodes instead of inserting user input as HTML.
3. Understand the storage boundary
Storage belongs to the website origin and browser profile. Switching between `localhost` and `127.0.0.1`, changing the port or using a different browser can show a different list. Clearing browser data removes it. Do not store passwords or private authentication secrets in this exercise's reading-list storage.
The error message distinguishes saving from merely updating the visible list. This matters when browser policies or storage limits prevent persistence. The current session remains usable, but the app explains that a reload may lose the changes.
Extension
Add a completed flag to each item. That requires changing the stored structure from strings to objects and deciding how old saved strings will be migrated. Handle that transition explicitly rather than assuming every visitor starts with an empty list.