Chrome Extension Website Annotator — Developer Guide

9 min read

Build a Website Annotator Extension

This tutorial guides you through building a Chrome extension that allows users to add sticky notes to any position on a webpage, save annotations per URL, and manage their notes.

Project Overview

The Website Annotator extension enables users to:

Step 1: Manifest Configuration

Create your manifest.json with the required permissions:

{
  "name": "Website Annotator",
  "version": "1.0",
  "manifest_version": 3,
  "permissions": [
    "activeTab",
    "storage"
  ],
  "content_scripts": [{
    "matches": ["<all_urls>"],
    "js": ["content.js"]
  }],
  "action": {
    "default_popup": "popup.html"
  }
}

We use activeTab permission for minimal access - the extension only works on pages the user explicitly activates.

Step 2: Content Script Setup

Create content.js to handle note creation and positioning:

// content.js - Main content script
let notes = [];
const NOTES_KEY = 'page_notes_';

function init() {
  const url = window.location.href;
  loadNotes(url);
  document.addEventListener('click', handleClick);
}

function handleClick(e) {
  // Only create note if not clicking on existing note
  if (e.target.closest('.annotator-note')) return;
  createNote(e.pageX, e.pageY, getCurrentUrl());
}

Step 3: Click-to-Add Notes

When users click on a page, create a note at that position:

function createNote(x, y, url) {
  const note = document.createElement('div');
  note.className = 'annotator-note';
  note.style.left = x + 'px';
  note.style.top = y + 'px';
  note.innerHTML = `
    <textarea placeholder="Add your note..."></textarea>
    <div class="controls">
      <button class="delete">×</button>
      <button class="color" data-color="yellow"></button>
      <button class="color" data-color="green"></button>
      <button class="color" data-color="blue"></button>
    </div>
  `;
  document.body.appendChild(note);
  notes.push({ element: note, x, y, url });
  saveNotes(url);
}

Step 4: Note UI Components

Style your notes with a floating card design:

.annotator-note {
  position: absolute;
  background: #fff9c4;
  border-radius: 8px;
  padding: 12px;
  width: 200px;
  box-shadow: 0 2px 10px rgba(0,0,0,0.2);
  z-index: 2147483647;
  font-family: sans-serif;
}

.annotator-note textarea {
  width: 100%;
  border: none;
  background: transparent;
  resize: vertical;
  min-height: 60px;
  font-size: 14px;
}

Step 5: Draggable Notes

Implement drag functionality using mouse events:

let draggedNote = null;
let offsetX, offsetY;

document.addEventListener('mousedown', (e) => {
  const note = e.target.closest('.annotator-note');
  if (!note) return;
  
  draggedNote = note;
  const rect = note.getBoundingClientRect();
  offsetX = e.clientX - rect.left;
  offsetY = e.clientY - rect.top;
});

document.addEventListener('mousemove', (e) => {
  if (!draggedNote) return;
  
  draggedNote.style.left = (e.pageX - offsetX) + 'px';
  draggedNote.style.top = (e.pageY - offsetY) + 'px';
});

document.addEventListener('mouseup', () => {
  if (draggedNote) {
    saveNotes(getCurrentUrl());
    draggedNote = null;
  }
});

Step 6: Position Storage

Save note positions relative to viewport for consistent placement:

function saveNotes(url) {
  const noteData = notes.map(n => ({
    x: parseFloat(n.element.style.left),
    y: parseFloat(n.element.style.top),
    text: n.element.querySelector('textarea').value,
    color: n.element.dataset.color || 'yellow'
  }));
  
  chrome.storage.local.set({
    [NOTES_KEY + btoa(url)]: noteData
  });
}

function loadNotes(url) {
  const key = NOTES_KEY + btoa(url);
  chrome.storage.local.get([key], (result) => {
    const saved = result[key] || [];
    saved.forEach(data => restoreNote(data, url));
  });
}

Step 7: Persistence Across Page Visits

Notes are automatically restored when revisiting a page:

function restoreNote(data, url) {
  const note = document.createElement('div');
  note.className = 'annotator-note';
  note.style.left = data.x + 'px';
  note.style.top = data.y + 'px';
  note.dataset.color = data.color;
  note.style.background = getColor(data.color);
  note.innerHTML = `<textarea>${data.text}</textarea>`;
  document.body.appendChild(note);
  notes.push({ element: note, x: data.x, y: data.y, url });
}

function getColor(colorName) {
  const colors = { yellow: '#fff9c4', green: '#c8e6c9', blue: '#bbdefb' };
  return colors[colorName] || colors.yellow;
}

Step 8: Note List in Popup

Create popup.html to show all annotated pages:

<style>
  body { width: 300px; padding: 10px; }
  .page-item { 
    padding: 8px; 
    border-bottom: 1px solid #eee; 
    cursor: pointer;
  }
  .page-item:hover { background: #f5f5f5; }
</style>
<div id="pages"></div>
<script src="popup.js"></script>
// popup.js - List all annotated pages
chrome.storage.local.get(null, (items) => {
  const pages = Object.keys(items)
    .filter(k => k.startsWith('page_notes_'))
    .map(k => atob(k.replace('page_notes_', '')));
  
  document.getElementById('pages').innerHTML = pages
    .map(url => `<div class="page-item">${new URL(url).hostname}</div>`)
    .join('');
});

Handling Page Layout Changes

When page layouts change, notes may appear misplaced. Anchor notes to DOM elements:

function anchorToElement(note, selector) {
  const element = document.querySelector(selector);
  if (element) {
    const rect = element.getBoundingClientRect();
    note.style.top = (rect.bottom + 10) + 'px';
    note.style.left = rect.left + 'px';
  }
}

Export Notes as Markdown

Allow users to export all notes:

function exportAsMarkdown() {
  chrome.storage.local.get(null, (items) => {
    let md = '# My Annotations\n\n';
    Object.entries(items).forEach(([key, notes]) => {
      if (!key.startsWith('page_notes_')) return;
      const url = atob(key.replace('page_notes_', ''));
      md += `## ${url}\n\n`;
      notes.forEach((n, i) => {
        md += `${i + 1}. ${n.text}\n`;
      });
      md += '\n';
    });
    // Download as file or copy to clipboard
  });
}

Shadow DOM Isolation

Use Shadow DOM to prevent page styles from affecting notes:

function createNoteWithShadow() {
  const host = document.createElement('div');
  const shadow = host.attachShadow({ mode: 'open' });
  shadow.innerHTML = `
    <style>
      :host { all: initial; }
      .note { ... }
    </style>
    <div class="note">...</div>
  `;
  document.body.appendChild(host);
}

Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.

No previous article
No next article