DocsHub
JavascriptAdvanced

Local Storage Notes

Build a notes app where users can create, edit, and delete notes that persist in localStorage.

Local Storage Notes

Problem

Build a notes app where the user can create, edit, and delete notes. All notes are saved to localStorage so they survive page refreshes. Each note shows when it was last updated.

Page loads
→ Existing notes loaded from localStorage
→ If no notes — show empty state

User clicks "New Note"
→ A blank note appears in edit mode

User types a title and content
→ Note is auto-saved as they type (debounced)

User clicks another note
→ Previous note saved, new note opens in editor

User clicks delete on a note
→ Note removed from list and localStorage

Page refresh
→ All notes still there exactly as left

Logic

  1. Load notes from localStorage on page load
  2. Render the notes list in the sidebar
  3. Clicking a note opens it in the editor
  4. Clicking "New Note" creates a blank note and opens it
  5. Auto-save the note as the user types — debounced 600ms
  6. Deleting a note removes it from the array and localStorage
  7. Show a formatted "last updated" timestamp on each note

Flow

yes no Page loads Load notes from localStorage Render notes sidebar Notes exist? Open first note in editor Show empty state User clicks New Note Create blank note Add to array and save User types in editor Debounce 600ms Update note in array Save to localStorage User clicks delete Remove from array

HTML Structure

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Notes App</title>
    <style>
      * {
        box-sizing: border-box;
        margin: 0;
        padding: 0;
      }

      body {
        font-family: sans-serif;
        display: flex;
        height: 100vh;
        background: #f5f5f5;
      }

      /* left sidebar */
      .sidebar {
        width: 260px;
        background: #fff;
        border-right: 1px solid #e0e0e0;
        display: flex;
        flex-direction: column;
        flex-shrink: 0;
      }

      /* sidebar header */
      .sidebar-header {
        padding: 20px 16px 14px;
        border-bottom: 1px solid #e0e0e0;
      }

      .sidebar-header h2 {
        font-size: 1rem;
        color: #111;
        margin-bottom: 12px;
      }

      /* new note button */
      #new-note-btn {
        width: 100%;
        padding: 9px;
        font-size: 0.9rem;
        font-weight: 600;
        background: #111;
        color: #fff;
        border: none;
        border-radius: 7px;
        cursor: pointer;
      }

      #new-note-btn:hover {
        opacity: 0.85;
      }

      /* scrollable notes list */
      #notes-list {
        flex: 1;
        overflow-y: auto;
        padding: 10px 8px;
      }

      /* each note item in sidebar */
      .note-item {
        padding: 10px 12px;
        border-radius: 8px;
        cursor: pointer;
        margin-bottom: 4px;
        position: relative;
        transition: background 0.15s;
      }

      .note-item:hover {
        background: #f5f5f5;
      }

      /* active note highlighted */
      .note-item.active {
        background: #f0f0f0;
      }

      .note-item-title {
        font-size: 0.9rem;
        font-weight: 600;
        color: #222;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
        margin-bottom: 3px;
        /* leave room for delete button */
        padding-right: 24px;
      }

      .note-item-preview {
        font-size: 0.78rem;
        color: #999;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
      }

      .note-item-date {
        font-size: 0.72rem;
        color: #bbb;
        margin-top: 3px;
      }

      /* delete button on hover */
      .note-delete-btn {
        position: absolute;
        top: 8px;
        right: 8px;
        background: none;
        border: none;
        cursor: pointer;
        color: #ccc;
        font-size: 0.9rem;
        display: none;
        padding: 2px 4px;
      }

      .note-item:hover .note-delete-btn {
        display: block;
      }

      .note-delete-btn:hover {
        color: #e74c3c;
      }

      /* right editor area */
      .editor {
        flex: 1;
        display: flex;
        flex-direction: column;
        padding: 32px 40px;
        overflow-y: auto;
      }

      /* note title input */
      #note-title {
        width: 100%;
        font-size: 1.6rem;
        font-weight: 700;
        border: none;
        outline: none;
        background: none;
        color: #111;
        margin-bottom: 8px;
        padding: 0;
      }

      #note-title::placeholder {
        color: #ddd;
      }

      /* last updated label */
      #note-date {
        font-size: 0.8rem;
        color: #bbb;
        margin-bottom: 20px;
      }

      /* divider between title and content */
      .divider {
        border: none;
        border-top: 1px solid #e8e8e8;
        margin-bottom: 20px;
      }

      /* note content textarea */
      #note-content {
        flex: 1;
        width: 100%;
        border: none;
        outline: none;
        background: none;
        font-size: 1rem;
        color: #444;
        resize: none;
        line-height: 1.7;
        font-family: inherit;
      }

      #note-content::placeholder {
        color: #ddd;
      }

      /* auto-save indicator */
      #save-indicator {
        font-size: 0.78rem;
        color: #bbb;
        text-align: right;
        margin-top: 12px;
        min-height: 16px;
      }

      /* empty state */
      #empty-state {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        flex: 1;
        color: #ccc;
        text-align: center;
        gap: 10px;
      }

      #empty-state p {
        font-size: 0.95rem;
      }
    </style>
  </head>
  <body>

    <!-- left sidebar -->
    <div class="sidebar">
      <div class="sidebar-header">
        <h2>📝 Notes</h2>
        <button id="new-note-btn">+ New Note</button>
      </div>
      <div id="notes-list"></div>
    </div>

    <!-- right editor — shown when a note is open -->
    <div class="editor" id="editor" style="display:none">
      <input type="text" id="note-title" placeholder="Note title..." />
      <div id="note-date"></div>
      <hr class="divider" />
      <textarea id="note-content" placeholder="Start writing..."></textarea>
      <div id="save-indicator"></div>
    </div>

    <!-- empty state — shown when no note is selected -->
    <div id="empty-state">
      <p>No note selected.</p>
      <p>Click <strong>+ New Note</strong> to get started.</p>
    </div>

    <script src="script.js"></script>
  </body>
</html>

Solution

// Step 1 — select elements
const newNoteBtn = document.querySelector("#new-note-btn");
const notesList = document.querySelector("#notes-list");
const editor = document.querySelector("#editor");
const emptyState = document.querySelector("#empty-state");
const noteTitleInput = document.querySelector("#note-title");
const noteContentInput = document.querySelector("#note-content");
const noteDateEl = document.querySelector("#note-date");
const saveIndicator = document.querySelector("#save-indicator");

// Step 2 — load notes from localStorage
// parse the JSON string back to an array — default to empty array
let notes = JSON.parse(localStorage.getItem("notes")) || [];

// Step 3 — track the id of the currently open note
let activeNoteId = null;

// Step 4 — render sidebar and open first note on load
renderSidebar();
if (notes.length > 0) {
  openNote(notes[0].id);
}

// Step 5 — new note button
newNoteBtn.addEventListener("click", () => {
  // create a new blank note
  const newNote = {
    id: Date.now(),              // unique id from timestamp
    title: "",
    content: "",
    updatedAt: new Date().toISOString()
  };

  // add to the beginning of the array so it shows at the top
  notes.unshift(newNote);
  saveToStorage();
  renderSidebar();
  openNote(newNote.id);

  // focus the title so user can start typing immediately
  noteTitleInput.focus();
});

// Step 6 — debounce function to delay auto-save
function debounce(fn, delay) {
  let timer;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

// Step 7 — auto-save when user types — debounced 600ms
const autoSave = debounce(() => {
  if (!activeNoteId) return;

  const note = notes.find((n) => n.id === activeNoteId);
  if (!note) return;

  // update note from editor values
  note.title = noteTitleInput.value;
  note.content = noteContentInput.value;
  note.updatedAt = new Date().toISOString();

  saveToStorage();
  renderSidebar();
  updateDateDisplay(note.updatedAt);

  // briefly show "Saved" indicator
  saveIndicator.textContent = "✓ Saved";
  setTimeout(() => saveIndicator.textContent = "", 1500);
}, 600);

// attach auto-save to both inputs
noteTitleInput.addEventListener("input", autoSave);
noteContentInput.addEventListener("input", autoSave);

// Step 8 — open a note in the editor
function openNote(id) {
  const note = notes.find((n) => n.id === id);
  if (!note) return;

  activeNoteId = id;

  // show editor, hide empty state
  editor.style.display = "flex";
  emptyState.style.display = "none";

  // fill editor with note content
  noteTitleInput.value = note.title;
  noteContentInput.value = note.content;
  updateDateDisplay(note.updatedAt);
  saveIndicator.textContent = "";

  // highlight active note in sidebar
  renderSidebar();
}

// Step 9 — render the sidebar note list
function renderSidebar() {
  if (notes.length === 0) {
    notesList.innerHTML = `<p style="text-align:center;color:#ccc;font-size:0.85rem;padding:20px">No notes yet</p>`;
    return;
  }

  notesList.innerHTML = notes
    .map((note) => {
      const isActive = note.id === activeNoteId;
      const title = note.title || "Untitled";
      const preview = note.content.split("\n")[0] || "No content";
      const date = formatDate(note.updatedAt);

      return `
        <div class="note-item ${isActive ? "active" : ""}" data-id="${note.id}">
          <div class="note-item-title">${title}</div>
          <div class="note-item-preview">${preview}</div>
          <div class="note-item-date">${date}</div>
          <button class="note-delete-btn" data-id="${note.id}" title="Delete note">✕</button>
        </div>
      `;
    })
    .join("");
}

// Step 10 — event delegation on the notes list
// handles both note clicks (open) and delete button clicks
notesList.addEventListener("click", (e) => {
  // check if delete button was clicked first
  const deleteBtn = e.target.closest(".note-delete-btn");

  if (deleteBtn) {
    e.stopPropagation(); // prevent note from opening
    const id = Number(deleteBtn.dataset.id);
    deleteNote(id);
    return;
  }

  // otherwise check if a note item was clicked
  const noteItem = e.target.closest(".note-item");
  if (noteItem) {
    const id = Number(noteItem.dataset.id);
    openNote(id);
  }
});

// Step 11 — delete a note
function deleteNote(id) {
  notes = notes.filter((n) => n.id !== id);
  saveToStorage();

  // if the deleted note was open — close the editor
  if (activeNoteId === id) {
    activeNoteId = null;
    editor.style.display = "none";
    emptyState.style.display = "flex";

    // open the next available note if any
    if (notes.length > 0) {
      openNote(notes[0].id);
    }
  }

  renderSidebar();
}

// Step 12 — save notes array to localStorage
function saveToStorage() {
  localStorage.setItem("notes", JSON.stringify(notes));
}

// helper — updates the date display in the editor
function updateDateDisplay(isoString) {
  noteDateEl.textContent = `Last edited ${formatDate(isoString)}`;
}

// helper — formats ISO date string to a readable format
function formatDate(isoString) {
  const date = new Date(isoString);
  const now = new Date();
  const diffMs = now - date;
  const diffMins = Math.floor(diffMs / 60000);
  const diffHours = Math.floor(diffMs / 3600000);
  const diffDays = Math.floor(diffMs / 86400000);

  if (diffMins < 1) return "Just now";
  if (diffMins < 60) return `${diffMins}m ago`;
  if (diffHours < 24) return `${diffHours}h ago`;
  if (diffDays === 1) return "Yesterday";
  return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
}

Code Execution

Trace through clicking "New Note":

StepCodeResult
Create note{ id: 1710000000000, title: "", content: "", updatedAt: "..." }blank note
Add to arraynotes.unshift(newNote)appears at top
SavelocalStorage.setItem("notes", JSON.stringify(notes))persisted
Render sidebarnew note shown as "Untitled"visible
Open noteeditor fills with blank valueseditor visible
Focus titlenoteTitleInput.focus()cursor in title

Trace through typing title "Meeting Notes" (auto-save fires after 600ms):

StepCodeResult
input firesautoSave() calleddebounce timer starts
600ms passtimer firesauto-save runs
Update notenote.title = "Meeting Notes"title updated
Update timenote.updatedAt = new Date().toISOString()timestamp updated
Save to storagelocalStorage.setItem(...)persisted
Render sidebarsidebar shows "Meeting Notes"updated
Show indicatorsaveIndicator.textContent = "✓ Saved"shows for 1.5s

Trace through deleting a note:

StepCodeResult
Click delete btndeleteBtn founde.stopPropagation() called
Read idNumber(deleteBtn.dataset.id)note id
Filternotes.filter(n => n.id !== id)note removed
SavelocalStorage.setItem(...)removed from storage
Check activeactiveNoteId === idopen next note or show empty state

Output

Page loads with saved notes  → sidebar shows all notes, first one opens
Click "New Note"             → blank note opens, title focused
Type title and content       → auto-saves after 600ms, "✓ Saved" flashes
Click another note           → switches to that note in editor
Delete a note                → removed from sidebar and localStorage
Refresh page                 → all remaining notes still there

On this page