Passing the Active Tab to the Popup

Get the right tab inside an MV3 popup: tabs.query with currentWindow, what activeTab grants the popup, reading the URL without the tabs permission, asking the content script for page data, and popup windows.

Published October 2, 2026 Updated October 2, 2026 7 min read
Table of Contents

The popup needs to know which page the user is looking at: to show “Save this article”, to read the page’s title, or to send a command to the content script running there. The obvious call, chrome.tabs.query({ active: true }), returns one active tab per window — so with two windows open, the popup sometimes acts on the wrong page. Then tab.url comes back undefined because the extension lacks the tabs permission, and messages to the content script fail on pages where it never ran. Getting “the current tab” right in a popup takes three small decisions. This guide makes them. It belongs to extension popup architecture.

Which tab “current” means

The toolbar popup belongs to one browser window — the window whose toolbar the user clicked. The tab the user is looking at is the active tab of that window. chrome.tabs.query({ active: true, currentWindow: true }) asks for exactly that: inside a popup, currentWindow refers to the window the popup is attached to. Without currentWindow, the query returns the active tab of every window, and taking the first element is a coin flip on multi-window setups. lastFocusedWindow: true is subtly different — it refers to the most recently focused window, which is usually but not always the same. Opening the popup also grants activeTab for that tab, which gives the popup temporary access to its URL, title and content even without the tabs permission or host permissions.

Which query returns the tab the user sees?Decision tree comparing tabs.query with active only, with currentWindow, and with lastFocusedWindow, from inside a toolbar popup.How is the query scoped?active onlyOne tab per window[0] may be wrongBug with 2+ windowsintermittentcurrentWindowPopup's own windowalways correctUse thisin toolbar popupslastFocusedWindowLast focused windowusually sameFine in workernot inside popup
In a popup, currentWindow is the one that always means 'the window I was opened from'.

Step-by-step: the right tab, with the right access

1. Query the popup’s own window

1// popup.js
2const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
3if (!tab) throw new Error("no active tab");          // e.g. popup opened in an unusual context

Execution context: the toolbar popup. The query resolves within milliseconds and needs no permission to return the tab object. Without tabs, host access or an activeTab grant, sensitive fields — url, title, favIconUrl, pendingUrl — are omitted, but id, windowId, index and status are always present.

2. Rely on the activeTab grant for URL and title

1{ "permissions": ["activeTab", "scripting"] }
1// popup.js — after opening, activeTab has granted this tab
2console.log(tab.url, tab.title);                     // defined, even without "tabs"

Execution context: the manifest and the popup. Opening the toolbar popup counts as invoking the extension on the active tab, so activeTab grants access to that tab: its URL and title become readable, and chrome.scripting can inject into it. This is the lightest way to give a popup page context — no install warning. The grant lasts until the tab navigates to another origin or the tab closes. See activeTab vs host permissions.

What the popup can read about the active tabTab fields and page access available to the popup with no permissions, with activeTab, with host permission for the site, and with the tabs permission.NeedNo permissionactiveTabHost permissiontabstab.id, windowIdYesYesYesYestab.url, titleNoThis tabMatching sitesAll tabsInject scriptNoThis tabMatching sitesNoInstall warningNoneNonePer siteHistory
activeTab is enough for most popups — and costs no install warning.

3. Ask the content script for page data — or inject a function

 1// popup.js
 2async function pageInfo(tabId) {
 3  try {
 4    return await chrome.tabs.sendMessage(tabId, { type: "page:info" });       // content script present
 5  } catch {
 6    const [{ result }] = await chrome.scripting.executeScript({                // not present: inject once
 7      target: { tabId },
 8      func: () => ({
 9        title: document.title,
10        description: document.querySelector('meta[name="description"]')?.content ?? "",
11        selection: getSelection()?.toString().slice(0, 2000) ?? "",
12      }),
13    });
14    return result;
15  }
16}

Execution context: the popup, with the activeTab grant. If a content script already runs on the page, a message gets richer, up-to-date state from it. If not — a page loaded before install, or a site outside the content script’s matches — injecting a small function under the activeTab grant reads what the popup needs. Both paths fail on restricted pages; handle that as in handling injection errors on restricted pages.

Popup open on a page without a content scriptThe popup queries the active tab of its window, tries to message the content script, gets no receiver, injects a small function under activeTab, and renders the returned title and selection.PopupBrowserTabtabs.query(active, currentWindow)sendMessage(page:info)no receiving endexecuteScript(func){title, selection}
Message first, inject as a fallback — both work under the activeTab grant.

4. Pass the tab to the worker explicitly

1// popup.js
2await chrome.runtime.sendMessage({ type: "save", tabId: tab.id, url: tab.url, title: tab.title });
3
4// sw.js
5chrome.runtime.onMessage.addListener((msg, sender) => {
6  if (msg?.type !== "save" || sender.tab) return;     // only from extension pages, not content scripts
7  saveArticle(msg.tabId, msg.url, msg.title);
8});

Execution context: the popup and the service worker. When the popup asks the worker to act on a page, include the tab id. Messages from the popup have no sender.tab — it is not a tab — and the worker querying “the active tab” itself may race with the user switching tabs while the popup is open. Treating the popup’s choice as authoritative avoids acting on a different page.

5. Handle popup windows and side panels

1// panel.js — in a side panel or a popup window, "currentWindow" is not the user's page window
2const targetTabId = Number(new URLSearchParams(location.search).get("tab"));
3const [tab] = targetTabId
4  ? [await chrome.tabs.get(targetTabId)]
5  : await chrome.tabs.query({ active: true, lastFocusedWindow: true, windowType: "normal" });

Execution context: a side panel or a standalone extension window. In a separate popup window opened with chrome.windows.create, currentWindow is that window, whose only tab is your extension page. Pass the target tab id in the URL when opening, or query the last focused normal window. Side panels belong to a browser window, so currentWindow works there, but track tabs.onActivated because the panel stays open while the user switches tabs — see showing different side panel content per tab.

6. Keep the popup in sync if the tab changes underneath

The popup closes when focus leaves it, so the user cannot usually switch tabs while it is open — but keyboard shortcuts and programmatic activation can. If the popup holds a long form, listen for tabs.onActivated and tabs.onUpdated for the captured tab id and warn if the page navigated, so the user does not save a note against the wrong URL.

7. Test with multiple windows

Most “wrong tab” bugs only appear with two or more windows. Add an end-to-end test that opens two windows with different pages, focuses the second, opens the popup there, and asserts the popup shows the second window’s page.

Common mistakes

  • active: true without currentWindow. Wrong tab with multiple windows.
  • Requesting tabs for one URL. activeTab already grants it in the popup.
  • Querying the active tab in the worker on the popup’s behalf. Pass the popup’s tab id instead.
  • Assuming a content script is present. Fall back to injection.
  • Using currentWindow in a popup window. It refers to the extension’s own window.

Cross-browser variation

  • Chrome / Edge: currentWindow in a popup refers to its window; opening the popup grants activeTab.
  • Firefox: same semantics; activeTab is granted on popup open. Firefox popups can also be opened in a “panel” window with different window semantics when detached.
  • Safari: tabs.query with currentWindow works; access to tab.url may still require the user to have granted the site in Safari’s per-site prompt.

Verification

  1. Open two windows on different sites; open the popup in each and confirm it shows that window’s page.
  2. Remove tabs and host permissions; confirm tab.url is still available in the popup.
  3. Open the popup on a page loaded before install; confirm the injection fallback provides the title.
  4. Open the popup on chrome://extensions; confirm a clear “not available here” message.

FAQ

Does activeTab persist after the popup closes?

Yes, until the tab navigates to a different origin or closes, so the worker can still act on the tab right after the popup sends a command.

Can the popup read the tab’s cookies?

Not through activeTab. Cookies need the cookies permission and host access.

Why is tab.url sometimes an empty string?

The tab may still be loading its first navigation; pendingUrl holds the destination. Check status.

Other MV3 Architecture & Extension Lifecycle Resources