現在の絞り込み: tech blogすべてクリア

Can't manipulate the DOM in your Chrome extension? Understanding the relationship between popups and the scripting API

Introduction

Chrome extensions fall into two broad categories: those that open a popup window when you click the extension icon, and those that don’t.

This isn’t just a UI difference. It fundamentally changes how you use the scripting API and activeTab to manipulate the DOM. If you don’t understand this, you’ll end up wondering why your code isn’t working even though it looks correct. This article lays out the key points.


Prerequisites

  • activeTab — A permission that grants temporary access to the current tab only when the user clicks the extension icon.
  • scripting API — A mechanism to temporarily inject scripts into a page using chrome.scripting.executeScript().
  • Service Worker (background.js) — A background process in the browser. It cannot directly access the page’s DOM.

For more details, see:
Script injection and permission design in Chrome extensions — The risks of content_scripts and how the scripting API solves them


The short version

When the user clicks the extension icon, what happens is determined by whether you specify default_popup in manifest.json. It’s one or the other.

  With popup Without popup On click popup.html opens action.onClicked event fires DOM operations start from popup.js background.js Use case Displaying forms, reviewing/editing info One-click immediate execution

These two are mutually exclusive. If a popup is set, the onClicked event will not fire.

action.onClicked — Fired when an action icon is clicked. This event will not fire if the action has a popup.

Reference: chrome.action API — Chrome for Developers


Pattern 1: No popup — handle everything in background.js

What kind of extension is this?

Extensions that run a process immediately when the icon is clicked, with no user interaction. For example: toggling dark mode with one click, taking a screenshot of the current page, or changing the page background color.

manifest.json

Don’t specify default_popup.

{
  "manifest_version": 3,
  "name": "One Click Extension",
  "version": "1.0",
  "action": {
    "default_title": "Click to run"
  },
  "permissions": [
    "activeTab",
    "scripting"
  ],
  "background": {
    "service_worker": "background.js"
  }
}

Flow

User clicks the icon
  ↓
action.onClicked event fires (background.js)
  ↓
activeTab permission becomes active
  ↓
background.js injects a script into the page via executeScript()
  ↓
The function runs in the page context and returns the result to background.js

Code example

// background.js
chrome.action.onClicked.addListener(async (tab) => {
  const results = await chrome.scripting.executeScript({
    target: { tabId: tab.id },
    func: () => {
      // Runs in the page context
      document.body.style.backgroundColor = 'red';
      return { title: document.title };
    }
  });
  console.log(results[0].result);
});

The official Chrome activeTab tutorial uses this pattern.

Reference: Inject scripts into the active tab — Chrome for Developers


Pattern 2: With popup — handle everything in popup.js

What kind of extension is this?

Extensions that display a form or information after the icon is clicked, where the user reviews or interacts before the process runs. For example: a bookmark tool, a translation tool, or a password manager.

manifest.json

Specify default_popup.

{
  "manifest_version": 3,
  "name": "Popup Extension",
  "version": "1.0",
  "action": {
    "default_title": "Bookmark this",
    "default_popup": "popup.html"
  },
  "permissions": [
    "activeTab",
    "scripting"
  ],
  "background": {
    "service_worker": "background.js"
  }
}

Flow

User clicks the icon
  ↓
popup.html opens (action.onClicked does NOT fire)
  ↓
activeTab permission becomes active
  ↓
popup.js injects a script into the page via executeScript()
  ↓
The function runs in the page context and returns the result to popup.js
  ↓
popup.js displays the retrieved values in popup.html's DOM

Code example

<!-- popup.html -->
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body>
  <h3 id="title"></h3>
  <p id="url"></p>
  <p id="published_at"></p>
  <button id="save">Save</button>
  <script src="popup.js"></script>
</body>
</html>
// popup.js
document.addEventListener('DOMContentLoaded', async () => {
  // Get the current active tab
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });

  // Inject a script into the page to get metadata
  const results = await chrome.scripting.executeScript({
    target: { tabId: tab.id },
    func: () => {
      return {
        title: document.title,
        url: window.location.href,
        published_at: document.querySelector('meta[property="article:published_time"]')
          ?.getAttribute('content')
          ?.slice(0, 10) || ""
      };
    }
  });

  const metadata = results[0].result;

  // Display in popup.html's DOM
  // popup.js runs in the same renderer process as popup.html,
  // so you can manipulate the DOM directly with document.getElementById().
  document.getElementById('title').textContent = metadata.title;
  document.getElementById('url').textContent = metadata.url;
  document.getElementById('published_at').textContent = metadata.published_at;
});

Common points of confusion

Can I get metadata in background.js via onClicked and then display it in the popup?

No. When default_popup is set, the onClicked event doesn’t fire at all.

Can popup.js even call executeScript()?

Yes. The activeTab permission becomes active the moment popup.html opens, so popup.js can call chrome.scripting.executeScript() directly.

Don’t you always need popup.html?

No. For extensions that don’t need a popup window (the one-click-and-done type), you skip popup.html and handle everything with onClicked + background.js.


How to decide which pattern to use

Use no popup (background.js + onClicked) when:

  • No user interaction is needed after the click
  • The process is fire-and-forget
  • A badge or icon change is enough to show the result

Use a popup (popup.js) when:

  • You want to show retrieved information to the user
  • The user needs to fill in or edit a form before submitting
  • You want to provide multiple action buttons

References

Photo by Pawel Czerwinski on Unsplash