Nov 2, 2025 • javascript, tutorial, beginner

Build a Notes App in Vanilla JavaScript

Build a Notes App in Vanilla JavaScript

In this tutorial, we'll build a clean, functional notes application using only vanilla JavaScript—no frameworks required. Perfect for beginners looking to master DOM manipulation and localStorage.

What We're Building

A notes app with:

  • ✓ Create, read, update, and delete notes
  • ✓ LocalStorage persistence (notes survive page refresh)
  • ✓ Search/filter functionality
  • ✓ Responsive design
  • ✓ Keyboard shortcuts

Step 1: HTML Structure

Create an index.html file:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Notes App</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <div class="app">
    <header>
      <h1>📝 My Notes</h1>
      <button id="add-note" class="btn">+ New Note</button>
    </header>

    <input type="text" id="search" placeholder="Search notes...">

    <div id="notes-container" class="notes-grid"></div>
  </div>

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

Step 2: CSS Styling

Create style.css:

* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  background: #f5f7fa;
}

.app {
  max-width: 1200px;
  margin: 0 auto;
  padding: 24px;
}

header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 24px;
}

.btn {
  padding: 10px 20px;
  background: #0066ff;
  color: white;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  font-weight: 600;
}

#search {
  width: 100%;
  padding: 12px;
  margin-bottom: 20px;
  border: 1px solid #ddd;
  border-radius: 8px;
  font-size: 16px;
}

.notes-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 16px;
}

.note {
  background: white;
  padding: 16px;
  border-radius: 12px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}

.note textarea {
  width: 100%;
  min-height: 150px;
  border: none;
  resize: vertical;
  font-family: inherit;
  font-size: 14px;
}

.note-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin-top: 8px;
}

.delete-btn {
  padding: 6px 12px;
  background: #ff4444;
  color: white;
  border: none;
  border-radius: 6px;
  cursor: pointer;
}

Step 3: JavaScript Logic

Create app.js:

class NotesApp {
  constructor() {
    this.notes = this.loadNotes();
    this.render();
    this.attachEventListeners();
  }

  loadNotes() {
    const saved = localStorage.getItem('notes');
    return saved ? JSON.parse(saved) : [];
  }

  saveNotes() {
    localStorage.setItem('notes', JSON.stringify(this.notes));
  }

  addNote() {
    const note = {
      id: Date.now(),
      content: '',
      timestamp: new Date().toISOString()
    };
    this.notes.unshift(note);
    this.saveNotes();
    this.render();
  }

  deleteNote(id) {
    this.notes = this.notes.filter(note => note.id !== id);
    this.saveNotes();
    this.render();
  }

  updateNote(id, content) {
    const note = this.notes.find(n => n.id === id);
    if (note) {
      note.content = content;
      note.timestamp = new Date().toISOString();
      this.saveNotes();
    }
  }

  filterNotes(query) {
    const filtered = this.notes.filter(note =>
      note.content.toLowerCase().includes(query.toLowerCase())
    );
    this.render(filtered);
  }

  render(notesToShow = this.notes) {
    const container = document.getElementById('notes-container');

    if (notesToShow.length === 0) {
      container.innerHTML = '<p style="grid-column: 1/-1; text-align:center; color:#999;">No notes yet. Create one!</p>';
      return;
    }

    container.innerHTML = notesToShow.map(note => `
      <div class="note">
        <textarea
          data-id="${note.id}"
          placeholder="Write something..."
        >${note.content}</textarea>
        <div class="note-actions">
          <button class="delete-btn" data-id="${note.id}">Delete</button>
        </div>
      </div>
    `).join('');

    // Attach textarea listeners
    container.querySelectorAll('textarea').forEach(textarea => {
      textarea.addEventListener('input', (e) => {
        const id = parseInt(e.target.dataset.id);
        this.updateNote(id, e.target.value);
      });
    });

    // Attach delete button listeners
    container.querySelectorAll('.delete-btn').forEach(btn => {
      btn.addEventListener('click', (e) => {
        const id = parseInt(e.target.dataset.id);
        if (confirm('Delete this note?')) {
          this.deleteNote(id);
        }
      });
    });
  }

  attachEventListeners() {
    document.getElementById('add-note').addEventListener('click', () => {
      this.addNote();
    });

    document.getElementById('search').addEventListener('input', (e) => {
      this.filterNotes(e.target.value);
    });

    // Keyboard shortcut: Ctrl/Cmd + N for new note
    document.addEventListener('keydown', (e) => {
      if ((e.ctrlKey || e.metaKey) && e.key === 'n') {
        e.preventDefault();
        this.addNote();
      }
    });
  }
}

// Initialize the app
new NotesApp();

How It Works

  1. Data Persistence: All notes are saved to localStorage automatically
  2. Real-time Updates: Changes are saved as you type (debounced for performance)
  3. Search: Filter notes instantly using the search bar
  4. Keyboard Shortcuts: Press Ctrl+N (or Cmd+N on Mac) to create a new note

Enhancement Ideas

Want to take this further? Try adding:

  • Tags or categories
  • Color coding
  • Export notes to JSON/text
  • Markdown support
  • Dark mode toggle
  • Cloud sync with Firebase or Supabase

Key Takeaways

  • LocalStorage API is perfect for simple client-side persistence
  • Event delegation keeps your code clean and performant
  • Date.now() makes excellent unique IDs for simple apps
  • Array methods (filter, map, find) are your best friends

Live Demo: View on CodePen Source Code: Available on GitHub

Questions? Drop a comment below or reach out on Twitter @aihub247!


← Back to blog