DEV Community

Ansh Sheladiya
Ansh Sheladiya

Posted on

Building Chrome Extensions with JavaScript: A Complete Guide

Chrome extensions let developers customize the browser, automate repetitive tasks, and build useful tools that improve everyday workflows. With JavaScript, HTML, and CSS, you can create everything from productivity helpers to advanced browser integrations without building a standalone application.

Modern Chrome extensions use Manifest V3, which introduces an event-driven service worker architecture and stronger security practices. Understanding the manifest, popup scripts, content scripts, and Chrome APIs gives you the foundation to build reliable extensions that work seamlessly with the browser.

In this guide, we'll build a practical Chrome extension that analyzes the current webpage, counts its words, estimates reading time, and saves useful page information. You'll learn how the main extension components communicate and how to test your implementation locally.

Understanding Chrome Extension Architecture

A Chrome extension is a small application that extends browser functionality through a collection of files and browser APIs. Its manifest.json file defines the extension's name, version, permissions, entry points, and supported capabilities. In Manifest V3, the background service worker handles browser events, popup scripts control the extension's interface, and content scripts interact with webpages.

Our example uses a popup interface to analyze the active tab's content. When the user clicks the Analyze Page button, the popup checks the active tab, verifies that the URL supports script injection, and executes JavaScript in the page's main document context through chrome.scripting.executeScript. The resulting data includes the page title, URL, word count, character count, and estimated reading time.

To build the extension, create a project folder named page-insights with a manifest.json file and the JavaScript files shown below. The manifest declares the activeTab, scripting, and storage permissions, which support user-triggered page inspection and saving analysis results. Using activeTab instead of requesting broad access to every website helps keep the extension's permissions limited.

The extension also includes a background service worker that listens for installation events and initializes a small amount of local storage. The popup stores each successful analysis using chrome.storage.local, allowing users to retain their latest result between popup sessions. After creating the files, open chrome://extensions, enable Developer mode, select Load unpacked, and choose the project folder.

// File: background.js
// Runs as the extension's event-driven service worker.

console.log("Page Insights service worker started.");

chrome.runtime.onInstalled.addListener(async (details) => {
  console.log("Extension installation event:", details.reason);

  // Initialize extension settings only when they do not exist.
  const settings = await chrome.storage.local.get("settings");

  if (!settings.settings) {
    await chrome.storage.local.set({
      settings: {
        saveHistory: true,
        version: "1.0.0"
      }
    });

    console.log("Default extension settings initialized.");
  }
});

// File: popup.js
// Handles the popup UI and requests webpage analysis.

document.addEventListener("DOMContentLoaded", () => {
  const analyzeButton = document.getElementById("analyzeButton");
  const statusElement = document.getElementById("status");
  const resultsElement = document.getElementById("results");
  const saveButton = document.getElementById("saveButton");

  let latestAnalysis = null;

  console.log("Popup initialized. Waiting for user interaction.");

  // Display a message without inserting webpage-provided HTML.
  function setStatus(message) {
    statusElement.textContent = message;
  }

  // Render the analysis using safe DOM text operations.
  function renderResults(data) {
    resultsElement.replaceChildren();

    const fields = [
      ["Page title", data.title],
      ["Word count", String(data.wordCount)],
      ["Character count", String(data.characterCount)],
      ["Reading time", `${data.readingTimeMinutes} minute(s)`],
      ["Page URL", data.url]
    ];

    for (const [label, value] of fields) {
      const row = document.createElement("p");
      const strong = document.createElement("strong");

      strong.textContent = `${label}: `;
      row.append(strong, document.createTextNode(value));
      resultsElement.appendChild(row);
    }
  }

  // Analyze the currently active tab after an explicit user click.
  analyzeButton.addEventListener("click", async () => {
    analyzeButton.disabled = true;
    saveButton.disabled = true;
    setStatus("Analyzing the current page...");
    resultsElement.replaceChildren();

    try {
      console.log("Step 1: Finding the active browser tab.");

      const [tab] = await chrome.tabs.query({
        active: true,
        currentWindow: true
      });

      if (!tab || !tab.id || !tab.url) {
        throw new Error("No active browser tab was found.");
      }

      // Chrome restricts injection into internal browser pages.
      const pageUrl = new URL(tab.url);

      if (!["http:", "https:"].includes(pageUrl.protocol)) {
        throw new Error("Open a regular HTTP or HTTPS webpage first.");
      }

      console.log("Step 2: Extracting webpage information.");

      const injectionResults = await chrome.scripting.executeScript({
        target: { tabId: tab.id },
        func: () => {
          // This function executes in the target webpage.
          const title = document.title || "Untitled page";
          const url = window.location.href;
          const text = document.body?.innerText || "";
          const words = text.trim().match(/\S+/g) || [];

          return {
            title,
            url,
            wordCount: words.length,
            characterCount: text.length,
            // Approximate reading speed: 200 words per minute.
            readingTimeMinutes: Math.max(
              1,
              Math.ceil(words.length / 200)
            ),
            analyzedAt: new Date().toISOString()
          };
        }
      });

      const result = injectionResults[0]?.result;

      if (!result) {
        throw new Error("The page did not return analysis results.");
      }

      console.log("Step 3: Analysis completed successfully.");
      console.log("Extracted analysis:", result);

      latestAnalysis = result;
      renderResults(result);
      saveButton.disabled = false;
      setStatus("Analysis completed successfully.");
    } catch (error) {
      // Permission restrictions and inaccessible tabs are handled here.
      console.error("Page analysis failed:", error);
      setStatus(error.message || "Unable to analyze this page.");
    } finally {
      analyzeButton.disabled = false;
    }
  });

  // Save the most recent analysis in extension-local storage.
  saveButton.addEventListener("click", async () => {
    if (!latestAnalysis) {
      setStatus("Analyze a page before saving its results.");
      return;
    }

    try {
      console.log("Step 4: Saving analysis to local extension storage.");

      const stored = await chrome.storage.local.get("history");
      const history = Array.isArray(stored.history)
        ? stored.history
        : [];

      // Keep the most recent 20 results to limit storage growth.
      const updatedHistory = [latestAnalysis, ...history].slice(0, 20);

      await chrome.storage.local.set({ history: updatedHistory });

      console.log("Step 5: Analysis saved.", updatedHistory.length);
      setStatus("Analysis saved locally.");
    } catch (error) {
      console.error("Failed to save analysis:", error);
      setStatus("Could not save the analysis.");
    }
  });
});

// File: manifest.json
// Keep this JSON in a separate file, not inside popup.js.
// {
//   "manifest_version": 3,
//   "name": "Page Insights",
//   "version": "1.0.0",
//   "description": "Analyze webpage text and estimate reading time.",
//   "permissions": ["activeTab", "scripting", "storage"],
//   "background": { "service_worker": "background.js" },
//   "action": {
//     "default_popup": "popup.html",
//     "default_title": "Page Insights"
//   }
// }

// File: popup.html
// Create a separate popup.html file with the following markup.
// <!DOCTYPE html>
// <html lang="en">
// <head>
//   <meta charset="UTF-8">
//   <meta name="viewport" content="width=device-width, initial-scale=1.0">
//   <title>Page Insights</title>
// </head>
// <body>
//   <h1>Page Insights</h1>
//   <button id="analyzeButton">Analyze Page</button>
//   <div id="results"></div>
//   <button id="saveButton" disabled>Save Analysis</button>
//   <p id="status" role="status">Ready to analyze a page.</p>
//   <script src="popup.js"></script>
// </body>
// </html>

// Testing checklist:
// 1. Save each file separately in the project directory.
// 2. Load the directory using chrome://extensions.
// 3. Open a normal webpage and click the extension icon.
// 4. Select Analyze Page and inspect the popup results.
// 5. Select Save Analysis and inspect chrome.storage.local.
// 6. Check the extension's service worker console for installation logs.
Enter fullscreen mode Exit fullscreen mode

Conclusion

Building Chrome extensions with JavaScript is a practical way to turn browser APIs into useful, focused products. By separating the manifest, popup interface, service worker, and page-injection logic, you create an architecture that is easier to maintain, debug, and expand.

This Page Insights example demonstrates user-triggered script injection, safe DOM rendering, asynchronous API calls, error handling, and persistent local storage. Before publishing an extension, test it across different websites, review permission requirements, handle restricted browser pages, and avoid collecting sensitive information unnecessarily.

Once the foundation is working, consider adding saved-history viewing, CSV export, configurable reading speeds, or a polished interface built with your preferred frontend stack. Start with one clearly defined problem, keep permissions minimal, and improve the extension based on real user feedback.

Top comments (0)