Skip to content

JavaScript Tutorial Practical Projects

Project 11: A Notes App with localStorage

Lesson 250 of 258 2 min read By Deepak Paswan Updated Oct 4, 2026

A notes app shows how to keep data between visits with localStorage. Notes are stored as JSON, and the app handles missing or damaged data.

> In the Try it editor, storage lives in memory for each run. On a real page, the notes survive reloading.

index.html
HTML
<style>
  .notes { font-family: system-ui, sans-serif; max-width: 340px; }
  textarea { width: 100%; box-sizing: border-box; padding: 8px; font: inherit; min-height: 60px; }
  button { font: inherit; padding: 6px 12px; cursor: pointer; }
  .note { border: 1px solid #ddd; border-radius: 8px; padding: 8px; margin: 8px 0; }
  .note small { color: #666; display: block; margin-bottom: 4px; }
  .note p { margin: 0 0 6px; white-space: pre-wrap; }
</style>

<div class="notes">
  <label for="text">New note</label>
  <textarea id="text"></textarea>
  <button id="save">Save note</button>
  <p id="msg" role="status"></p>
  <div id="list"></div>
</div>

<script>
  const KEY = "notes-app:v1";

  function loadNotes() {
    try {
      const data = JSON.parse(localStorage.getItem(KEY));
      return Array.isArray(data) ? data : [];           // ignore anything that is not an array
    } catch {
      return [];
    }
  }

  function saveNotes(notes) {
    try {
      localStorage.setItem(KEY, JSON.stringify(notes));
      return true;
    } catch {
      return false;                                      // storage full or unavailable
    }
  }

  let notes = loadNotes();
  let editingId = null;

  const list = document.querySelector("#list");
  const text = document.querySelector("#text");
  const msg = document.querySelector("#msg");

  function render() {
    list.replaceChildren(...notes.map(note => {
      const wrap = document.createElement("div");
      wrap.className = "note";
      wrap.dataset.id = note.id;

      const when = document.createElement("small");
      when.textContent = new Date(note.updated).toLocaleString();

      const body = document.createElement("p");
      body.textContent = note.text;

      const edit = document.createElement("button");
      edit.textContent = "Edit";
      edit.dataset.action = "edit";
      const del = document.createElement("button");
      del.textContent = "Delete";
      del.dataset.action = "delete";

      wrap.append(when, body, edit, " ", del);
      return wrap;
    }));
  }

  document.querySelector("#save").addEventListener("click", () => {
    const value = text.value.trim();
    if (!value) { msg.textContent = "Write something first."; return; }

    if (editingId === null) {
      notes = [{ id: Date.now(), text: value, updated: Date.now() }, ...notes];
    } else {
      notes = notes.map(n => (n.id === editingId ? { ...n, text: value, updated: Date.now() } : n));
      editingId = null;
    }

    msg.textContent = saveNotes(notes) ? "Saved." : "Could not save to this browser.";
    text.value = "";
    render();
  });

  list.addEventListener("click", event => {
    const button = event.target.closest("button[data-action]");
    if (!button) return;
    const id = Number(button.closest(".note").dataset.id);

    if (button.dataset.action === "delete") {
      notes = notes.filter(n => n.id !== id);
      saveNotes(notes);
      render();
    } else {
      editingId = id;
      text.value = notes.find(n => n.id === id).text;
      text.focus();
      msg.textContent = "Editing a note. Press Save to update it.";
    }
  });

  render();
</script>

The ideas used

  • loadNotes() / saveNotes() wrap every localStorage and JSON call in try...catch, so damaged data or a full store cannot crash the app (JSON with localStorage).
  • A namespaced, versioned key (notes-app:v1) avoids clashes and lets you migrate data later.
  • Notes carry an id and an updated timestamp.
  • Immutable updates with spread, map and filter.
  • Event delegation on the list, data- attributes for ids.
  • white-space: pre-wrap keeps line breaks, and textContent keeps text safe.

What not to store

Notes can contain private information, and localStorage is not encrypted and is readable by any script on the page. Do not use it for passwords or sensitive data (Sensitive Data). For notes that must be private or shared across devices, store them on a server.

Try changing it

  1. Add a search box (Search project).
  2. Auto-save a draft as the user types, with debouncing.
  3. Add export and import of notes as a JSON file.
  4. Show a warning near the storage limit (Storage Limits).