Build a Text Highlighter Chrome Extension: Annotate and Save Web Content

30 min read

Build a Text Highlighter Chrome Extension: Annotate and Save Web Content

Build a Text Highlighter Chrome Extension: Annotate and Save Web Content

Web annotation tools have become essential for researchers, students, and professionals who consume large amounts of online content. The ability to highlight, annotate, and save important passages directly in the browser transforms how we interact with web content. In this comprehensive guide, we will walk you through building a fully functional text highlighter Chrome extension using Manifest V3.

This extension will allow users to select text on any webpage, apply colored highlights, add notes to their selections, and save all annotations locally for future reference. By the end of this tutorial, you will have a complete understanding of how to build a text highlighter chrome extension that rivals popular commercial solutions.


Why Build a Text Highlighter Chrome Extension?

The demand for web annotation tools continues to grow as more people rely on online resources for research, learning, and professional development. A well-designed highlight text chrome extension solves real problems for users across many domains:

Market Demand and Use Cases

Researchers need to mark key findings across dozens of articles and later export or review those highlights. Students highlighting text chrome extension tools help them study more effectively by color-coding different topics or themes. Content creators use web annotation extension functionality to collect quotes and inspiration for their own work. Professionals bookmark and annotate industry news, competitor analysis, and market research for later reference.

Building a chrome extension annotate feature gives you a valuable tool that addresses genuine user pain points. The skills you develop — including DOM manipulation, cross-origin communication, storage management, and user interface design — transfer directly to many other extension projects.

Learning Opportunities

This project teaches you several important Chrome extension development concepts. You will master text selection APIs that let you detect and manipulate what users highlight. You will work with localStorage and the Chrome Storage API to persist user data across sessions. You will implement a popup interface that communicates with content scripts. Finally, you will handle message passing between different extension components to coordinate functionality.


Project Architecture and Design

Before writing any code, let us establish the architecture that will power our text highlighter chrome extension.

Core Components

Our extension consists of four main components that work together to deliver a seamless highlighting experience:

The Manifest File defines the extension’s identity, permissions, and the files that compose it. We will use Manifest V3, which is the current standard for Chrome extensions.

The Content Script runs in the context of web pages and handles text selection, highlight rendering, and DOM manipulation. This is where the magic happens when users select and highlight text.

The Background Service Worker manages extension lifecycle events and coordinates communication between components when needed.

The Popup Interface provides users with controls to manage their highlights, change highlight colors, view saved annotations, and access extension settings.

Data Flow

When a user selects text on a webpage, the content script detects the selection and displays a floating toolbar. The user chooses a highlight color and optionally adds a note. The content script then wraps the selected text in a highlight element, stores the highlight data in Chrome’s local storage, and communicates with the popup to update the saved highlights list.

This architecture separates concerns cleanly: the content script handles page interaction, the popup manages user controls, and the storage system persists data across sessions.


Step 1: Creating the Manifest

Every Chrome extension begins with the manifest.json file. This configuration tells Chrome about your extension’s capabilities and requirements:

{
  "manifest_version": 3,
  "name": "Text Highlighter Pro",
  "version": "1.0.0",
  "description": "Annotate and save web content with customizable highlights and notes",
  "permissions": [
    "storage",
    "activeTab",
    "scripting"
  ],
  "host_permissions": [
    "<all_urls>"
  ],
  "action": {
    "default_popup": "popup.html",
    "default_icon": {
      "16": "icons/icon16.png",
      "48": "icons/icon48.png",
      "128": "icons/icon128.png"
    }
  },
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content.js"],
      "css": ["content.css"],
      "run_at": "document_end"
    }
  ],
  "background": {
    "service_worker": "background.js"
  }
}

This manifest requests the minimum permissions needed for our text highlighter chrome extension to function. The storage permission allows us to save highlights persistently. The activeTab permission lets us interact with the currently active tab when needed. The scripting permission enables us to execute content scripts dynamically if required.

Notice that we use <all_urls> in host_permissions because users will want to highlight text on virtually any website they visit. However, we are transparent about this in the extension’s description to maintain user trust.


Step 2: Building the Content Script

The content script is the heart of our build highlighter extension. It detects text selections, renders highlights, and manages the floating toolbar that appears when users select text.

Detecting Text Selection

The first challenge is detecting when a user selects text on the page. We use the mouseup event combined with the Selection API:

// content.js
(() => {
  'use strict';

  // Store for active highlights on this page
  let highlights = [];
  let currentSelection = null;

  // Color palette for highlights
  const COLORS = [
    { name: 'Yellow', value: '#FFEB3B', alpha: '0.4' },
    { name: 'Green', value: '#4CAF50', alpha: '0.4' },
    { name: 'Blue', value: '#2196F3', alpha: '0.4' },
    { name: 'Pink', value: '#E91E63', alpha: '0.4' },
    { name: 'Orange', value: '#FF9800', alpha: '0.4' }
  ];

  // Listen for text selection
  document.addEventListener('mouseup', handleTextSelection);

  function handleTextSelection(event) {
    const selection = window.getSelection();
    
    // Check if there's actual selected text
    if (!selection || selection.rangeCount === 0 || selection.toString().trim() === '') {
      // Hide toolbar if clicking elsewhere
      hideToolbar();
      return;
    }

    const selectedText = selection.toString().trim();
    if (selectedText.length < 1) return;

    // Get the bounds of the selection for toolbar positioning
    const range = selection.getRangeAt(0);
    const rect = range.getBoundingClientRect();

    currentSelection = {
      text: selectedText,
      range: range.cloneRange(),
      rect: rect
    };

    // Show the floating toolbar
    showToolbar(rect);
  }

  function showToolbar(rect) {
    // Remove existing toolbar if any
    hideToolbar();

    const toolbar = document.createElement('div');
    toolbar.id = 'highlighter-toolbar';
    toolbar.innerHTML = `
      <div class="highlighter-colors">
        ${COLORS.map((color, index) => 
          `<button class="highlighter-color-btn" data-color="${color.value}" 
           data-alpha="${color.alpha}" style="background-color: ${color.value}" 
           title="${color.name}"></button>`
        ).join('')}
      </div>
      <button class="highlighter-note-btn" title="Add Note">📝</button>
      <button class="highlighter-clear-btn" title="Clear Selection">✕</button>
    `;

    // Style the toolbar
    toolbar.style.cssText = `
      position: fixed;
      z-index: 999999;
      display: flex;
      gap: 4px;
      padding: 6px 8px;
      background: white;
      border-radius: 8px;
      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      transform: translateX(-50%);
    `;

    // Position above selection
    toolbar.style.left = `${rect.left + (rect.width / 2)}px`;
    toolbar.style.top = `${rect.top - 45 + window.scrollY}px`;

    document.body.appendChild(toolbar);

    // Add event listeners
    setupToolbarEvents(toolbar);
  }

  function hideToolbar() {
    const toolbar = document.getElementById('highlighter-toolbar');
    if (toolbar) {
      toolbar.remove();
    }
  }

  function setupToolbarEvents(toolbar) {
    // Color buttons for highlighting
    toolbar.querySelectorAll('.highlighter-color-btn').forEach(btn => {
      btn.addEventListener('mouseup', (event) => {
        event.stopPropagation();
        const color = btn.dataset.color;
        const alpha = btn.dataset.alpha;
        applyHighlight(color, alpha);
      });
    });

    // Note button
    toolbar.querySelector('.highlighter-note-btn').addEventListener('mouseup', (event) => {
      event.stopPropagation();
      promptForNote();
    });

    // Clear button
    toolbar.querySelector('.highlighter-clear-btn').addEventListener('mouseup', (event) => {
      event.stopPropagation();
      window.getSelection().removeAllRanges();
      hideToolbar();
      currentSelection = null;
    });
  }

  function applyHighlight(color, alpha) {
    if (!currentSelection) return;

    const selection = window.getSelection();
    if (!selection.rangeCount) return;

    // Create highlight span
    const span = document.createElement('span');
    span.className = 'user-highlight';
    span.style.backgroundColor = color;
    span.style.backgroundAlpha = alpha;
    span.dataset.highlightId = generateId();
    span.dataset.color = color;

    // Wrap the selection
    try {
      const range = selection.getRangeAt(0);
      range.surroundContents(span);
      
      // Clear the selection
      selection.removeAllRanges();

      // Save the highlight
      const highlightData = {
        id: span.dataset.highlightId,
        text: currentSelection.text,
        color: color,
        note: '',
        url: window.location.href,
        title: document.title,
        timestamp: Date.now()
      };

      saveHighlight(highlightData);
      hideToolbar();
      
      // Notify popup to update
      chrome.runtime.sendMessage({ action: 'highlightAdded' });
      
    } catch (e) {
      console.error('Cannot highlight across block boundaries:', e);
      // For complex selections spanning multiple elements,
      // you would need a more sophisticated approach
    }
  }

  function promptForNote() {
    const note = prompt('Add a note for this highlight (optional):');
    if (note !== null) {
      currentSelection.note = note;
      applyHighlight('#FFEB3B', '0.4'); // Default yellow
      
      // Update the note in storage
      updateHighlightNote(currentSelection.text, note);
    }
  }

  function generateId() {
    return 'hl-' + Math.random().toString(36).substr(2, 9);
  }

  async function saveHighlight(highlightData) {
    // Get existing highlights from storage
    const result = await chrome.storage.local.get('highlights');
    const highlights = result.highlights || [];
    
    // Add new highlight
    highlights.push(highlightData);
    
    // Save back to storage
    await chrome.storage.local.set({ highlights });
  }

  async function updateHighlightNote(text, note) {
    const result = await chrome.storage.local.get('highlights');
    const highlights = result.highlights || [];
    
    const highlight = highlights.find(h => h.text === text);
    if (highlight) {
      highlight.note = note;
      await chrome.storage.local.set({ highlights });
    }
  }

  // Listen for messages from popup
  chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
    if (message.action === 'getHighlights') {
      sendResponse({ url: window.location.href });
    } else if (message.action === 'clearHighlights') {
      clearAllHighlights();
    }
  });

  function clearAllHighlights() {
    document.querySelectorAll('.user-highlight').forEach(el => {
      const parent = el.parentNode;
      while (el.firstChild) {
        parent.insertBefore(el.firstChild, el);
      }
      parent.removeChild(el);
    });
  }
})();

This content script handles the core functionality of our chrome extension text highlighter. It detects when users select text, displays a floating toolbar with color options, and applies highlights by wrapping selected text in styled span elements.

Adding CSS Styles

Create a content.css file to style both the highlights and the toolbar:

/* content.css */
.user-highlight {
  background-color: rgba(255, 235, 59, 0.4);
  border-radius: 2px;
  padding: 1px 0;
  cursor: pointer;
  transition: background-color 0.2s ease;
}

.user-highlight:hover {
  background-color: rgba(255, 235, 59, 0.6);
}

/* Toolbar styles */
#highlighter-toolbar {
  animation: fadeIn 0.15s ease-out;
}

@keyframes fadeIn {
  from {
    opacity: 0;
    transform: translateX(-50%) translateY(5px);
  }
  to {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
  }
}

.highlighter-color-btn {
  width: 24px;
  height: 24px;
  border: 2px solid transparent;
  border-radius: 50%;
  cursor: pointer;
  transition: transform 0.1s ease, border-color 0.1s ease;
}

.highlighter-color-btn:hover {
  transform: scale(1.15);
  border-color: #333;
}

.highlighter-note-btn,
.highlighter-clear-btn {
  background: #f5f5f5;
  border: none;
  border-radius: 4px;
  width: 28px;
  height: 28px;
  cursor: pointer;
  font-size: 14px;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background-color 0.1s ease;
}

.highlighter-note-btn:hover,
.highlighter-clear-btn:hover {
  background: #e0e0e0;
}

The popup provides users with access to their saved highlights and controls for managing annotations. Let us create the popup HTML and JavaScript:

<!-- popup.html -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Text Highlighter</title>
  <style>
    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }
    
    body {
      width: 360px;
      min-height: 400px;
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      background: #fafafa;
      color: #333;
    }

    .header {
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      color: white;
      padding: 16px;
      text-align: center;
    }

    .header h1 {
      font-size: 18px;
      font-weight: 600;
      margin-bottom: 4px;
    }

    .header p {
      font-size: 12px;
      opacity: 0.9;
    }

    .stats {
      display: flex;
      justify-content: space-around;
      padding: 12px;
      background: white;
      border-bottom: 1px solid #eee;
    }

    .stat {
      text-align: center;
    }

    .stat-value {
      font-size: 20px;
      font-weight: 700;
      color: #667eea;
    }

    .stat-label {
      font-size: 11px;
      color: #666;
    }

    .highlights-list {
      max-height: 300px;
      overflow-y: auto;
      padding: 8px;
    }

    .highlight-item {
      background: white;
      border-radius: 8px;
      padding: 12px;
      margin-bottom: 8px;
      box-shadow: 0 1px 3px rgba(0,0,0,0.08);
      border-left: 4px solid #ddd;
      cursor: pointer;
      transition: transform 0.1s ease, box-shadow 0.1s ease;
    }

    .highlight-item:hover {
      transform: translateY(-1px);
      box-shadow: 0 3px 8px rgba(0,0,0,0.12);
    }

    .highlight-text {
      font-size: 13px;
      line-height: 1.5;
      margin-bottom: 8px;
      display: -webkit-box;
      -webkit-line-clamp: 3;
      -webkit-box-orient: vertical;
      overflow: hidden;
    }

    .highlight-meta {
      display: flex;
      justify-content: space-between;
      align-items: center;
      font-size: 11px;
      color: #888;
    }

    .highlight-note {
      background: #fff8e1;
      padding: 6px 8px;
      border-radius: 4px;
      font-size: 12px;
      margin-top: 8px;
      color: #666;
    }

    .highlight-actions {
      display: flex;
      gap: 8px;
      margin-top: 8px;
    }

    .action-btn {
      background: #f5f5f5;
      border: none;
      padding: 4px 8px;
      border-radius: 4px;
      font-size: 11px;
      cursor: pointer;
      color: #666;
    }

    .action-btn:hover {
      background: #e0e0e0;
    }

    .action-btn.delete {
      color: #e53935;
    }

    .action-btn.delete:hover {
      background: #ffebee;
    }

    .empty-state {
      text-align: center;
      padding: 40px 20px;
      color: #888;
    }

    .empty-state-icon {
      font-size: 40px;
      margin-bottom: 12px;
    }

    .empty-state p {
      font-size: 13px;
    }

    .filter-tabs {
      display: flex;
      background: white;
      border-bottom: 1px solid #eee;
    }

    .filter-tab {
      flex: 1;
      padding: 10px;
      text-align: center;
      font-size: 12px;
      cursor: pointer;
      border-bottom: 2px solid transparent;
      color: #666;
    }

    .filter-tab.active {
      color: #667eea;
      border-bottom-color: #667eea;
      font-weight: 500;
    }
  </style>
</head>
<body>
  <div class="header">
    <h1>Text Highlighter Pro</h1>
    <p>Annotate and save web content</p>
  </div>
  
  <div class="stats">
    <div class="stat">
      <div class="stat-value" id="totalCount">0</div>
      <div class="stat-label">Total Highlights</div>
    </div>
    <div class="stat">
      <div class="stat-value" id="pageCount">0</div>
      <div class="stat-label">On This Page</div>
    </div>
  </div>

  <div class="filter-tabs">
    <div class="filter-tab active" data-filter="all">All Highlights</div>
    <div class="filter-tab" data-filter="current">Current Page</div>
  </div>

  <div class="highlights-list" id="highlightsList">
    <div class="empty-state">
      <div class="empty-state-icon">🖊️</div>
      <p>Select text on any page and click a color to highlight it.</p>
    </div>
  </div>

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

Now create the popup.js file to handle user interactions:

// popup.js
document.addEventListener('DOMContentLoaded', async () => {
  const highlightsList = document.getElementById('highlightsList');
  const totalCount = document.getElementById('totalCount');
  const pageCount = document.getElementById('pageCount');
  const filterTabs = document.querySelectorAll('.filter-tab');
  
  let allHighlights = [];
  let currentFilter = 'all';

  // Load highlights from storage
  async function loadHighlights() {
    const result = await chrome.storage.local.get('highlights');
    allHighlights = result.highlights || [];
    
    // Get current tab URL
    const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
    const currentUrl = tab ? tab.url : '';
    
    // Update counts
    totalCount.textContent = allHighlights.length;
    const pageHighlights = allHighlights.filter(h => h.url === currentUrl);
    pageCount.textContent = pageHighlights.length;
    
    // Apply filter
    renderHighlights(currentUrl);
  }

  function renderHighlights(currentUrl) {
    const filtered = currentFilter === 'all' 
      ? allHighlights 
      : allHighlights.filter(h => h.url === currentUrl);

    if (filtered.length === 0) {
      highlightsList.innerHTML = `
        <div class="empty-state">
          <div class="empty-state-icon">🖊️</div>
          <p>${currentFilter === 'all' 
            ? 'No highlights yet. Select text on any page to get started.' 
            : 'No highlights on this page yet.'}</p>
        </div>
      `;
      return;
    }

    // Sort by timestamp (newest first)
    filtered.sort((a, b) => b.timestamp - a.timestamp);

    highlightsList.innerHTML = filtered.map(highlight => `
      <div class="highlight-item" data-id="${highlight.id}" style="border-left-color: ${highlight.color}">
        <div class="highlight-text">${escapeHtml(highlight.text)}</div>
        ${highlight.note ? `<div class="highlight-note">📝 ${escapeHtml(highlight.note)}</div>` : ''}
        <div class="highlight-meta">
          <span>${formatDate(highlight.timestamp)}</span>
          <span>${new URL(highlight.url).hostname}</span>
        </div>
        <div class="highlight-actions">
          <button class="action-btn go-to" data-url="${highlight.url}">Go to</button>
          <button class="action-btn delete" data-id="${highlight.id}">Delete</button>
        </div>
      </div>
    `).join('');

    // Add event listeners
    setupHighlightEvents();
  }

  function setupHighlightEvents() {
    // Go to highlight
    document.querySelectorAll('.action-btn.go-to').forEach(btn => {
      btn.addEventListener('click', async (e) => {
        e.stopPropagation();
        const url = btn.dataset.url;
        const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
        if (tab) {
          chrome.tabs.update(tab.id, { url });
          window.close();
        }
      });
    });

    // Delete highlight
    document.querySelectorAll('.action-btn.delete').forEach(btn => {
      btn.addEventListener('click', async (e) => {
        e.stopPropagation();
        const id = btn.dataset.id;
        await deleteHighlight(id);
        loadHighlights();
      });
    });
  }

  async function deleteHighlight(id) {
    const result = await chrome.storage.local.get('highlights');
    const highlights = result.highlights || [];
    const filtered = highlights.filter(h => h.id !== id);
    await chrome.storage.local.set({ highlights: filtered });
  }

  // Filter tabs
  filterTabs.forEach(tab => {
    tab.addEventListener('click', async () => {
      filterTabs.forEach(t => t.classList.remove('active'));
      tab.classList.add('active');
      currentFilter = tab.dataset.filter;
      
      const [tabInfo] = await chrome.tabs.query({ active: true, currentWindow: true });
      renderHighlights(tabInfo ? tabInfo.url : '');
    });
  });

  // Listen for highlight additions
  chrome.runtime.onMessage.addListener((message) => {
    if (message.action === 'highlightAdded') {
      loadHighlights();
    }
  });

  // Initial load
  loadHighlights();
});

// Helper functions
function escapeHtml(text) {
  const div = document.createElement('div');
  div.textContent = text;
  return div.innerHTML;
}

function formatDate(timestamp) {
  const date = new Date(timestamp);
  const now = new Date();
  const diff = now - date;
  
  if (diff < 60000) return 'Just now';
  if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
  if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
  if (diff < 604800000) return `${Math.floor(diff / 86400000)}d ago`;
  
  return date.toLocaleDateString();
}

Step 4: Creating the Background Service Worker

The background service worker handles extension lifecycle events and can coordinate between different parts of your extension:

// background.js
chrome.runtime.onInstalled.addListener((details) => {
  console.log('Text Highlighter Pro installed:', details.reason);
  
  // Initialize storage if needed
  chrome.storage.local.get('highlights', (result) => {
    if (!result.highlights) {
      chrome.storage.local.set({ highlights: [] });
    }
  });
});

// Handle extension icon click (if no popup)
chrome.action.onClicked.addListener((tab) => {
  // This only fires when there is no popup defined
  // Since we have a popup, this won't be used
});

Step 5: Testing Your Extension

Now that we have built all the components, let us test our text highlighter chrome extension in Chrome:

  1. Open Chrome and navigate to chrome://extensions/
  2. Enable “Developer mode” in the top-right corner
  3. Click “Load unpacked” and select your extension’s root directory
  4. The extension will appear in your toolbar

To test the chrome extension annotate functionality:

  1. Navigate to any article or webpage with text content
  2. Select some text with your mouse
  3. A floating toolbar should appear above your selection
  4. Click a color button to apply the highlight
  5. Click the note button to add an annotation
  6. Click the extension icon to see your saved highlights in the popup

Common Issues and Solutions

Highlights not appearing: Check the console for errors. Make sure the content script is loading correctly and that there are no JavaScript errors in the page itself.

Toolbar positioning issues: The toolbar uses getBoundingClientRect() which works relative to the viewport. If your page has unusual CSS transforms or positioning, you may need to adjust the calculation.

Cannot highlight across elements: The current implementation uses surroundContents() which only works for selections within a single parent element. For more complex selections, you would need to use a range extraction library or implement a more sophisticated algorithm.


Step 6: Publishing to the Chrome Web Store

Once you have tested your extension thoroughly, you can publish it to reach millions of users:

Prepare for Submission

  1. Create icons: You need 16x16, 48x48, and 128x128 PNG icons
  2. Write a description: Clearly explain what your chrome extension text highlighter does
  3. Take screenshots: Show the highlighting in action on a real webpage
  4. Privacy policy: Required since you are storing user data

Submit for Review

  1. Zip your extension files (excluding test files and source maps)
  2. Go to the Chrome Web Store Developer Dashboard
  3. Create a new item and upload your zip
  4. Fill in the store listing details
  5. Submit for review

Google typically reviews submissions within 1-3 business days. Make sure your extension follows all policies, particularly regarding user data handling and privacy.


Advanced Features to Consider

Once you have the basic chrome extension annotate functionality working, consider adding these advanced features:

Export Functionality

Allow users to export their highlights in various formats: plain text, Markdown, HTML, or PDF. This is particularly valuable for researchers who need to compile notes from multiple sources.

Cloud Sync

Implement synchronization using chrome.storage.sync to let users access their highlights across different devices. This requires careful handling of merge conflicts and storage quotas.

Highlight Sharing

Allow users to share specific highlights via a unique URL that opens the original page with the highlight visible.

PDF Support

Use the Chrome PDF API to extend highlighting capability to PDF documents viewed in Chrome.

Search Functionality

Add a search feature in the popup to quickly find highlights by text content or notes.


Conclusion

You have now built a complete text highlighter chrome extension that allows users to select and annotate web content. This project demonstrates several essential Chrome extension development concepts including content script manipulation, the Selection API, local storage management, popup UI design, and inter-component communication.

The chrome extension highlight functionality you created provides real value to users who need to annotate and save web content. With the foundation in place, you can continue adding features like cloud sync, export options, and PDF support to make your extension even more powerful.

Remember to test thoroughly across different websites, handle edge cases gracefully, and follow Chrome Web Store policies when you are ready to publish. Your text highlighter extension has the potential to become an indispensable tool for researchers, students, and professionals who work with online content.

Start building today, and transform the way users interact with web content through your chrome extension annotate tools.


This guide is part of the Chrome Extension Guide by theluckystrike — your comprehensive resource for Chrome extension development.

No previous article
No next article