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.
Table of Contents
- Which tab “current” means
- Step-by-step: the right tab, with the right access
- 1. Query the popup’s own window
- 2. Rely on the activeTab grant for URL and title
- 3. Ask the content script for page data — or inject a function
- 4. Pass the tab to the worker explicitly
- 5. Handle popup windows and side panels
- 6. Keep the popup in sync if the tab changes underneath
- 7. Test with multiple windows
- Common mistakes
- Cross-browser variation
- Verification
- FAQ
- Related
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.
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.
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.
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: truewithoutcurrentWindow. Wrong tab with multiple windows.- Requesting
tabsfor one URL.activeTabalready 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
currentWindowin a popup window. It refers to the extension’s own window.
Cross-browser variation
- Chrome / Edge:
currentWindowin a popup refers to its window; opening the popup grantsactiveTab. - Firefox: same semantics;
activeTabis granted on popup open. Firefox popups can also be opened in a “panel” window with different window semantics when detached. - Safari:
tabs.querywithcurrentWindowworks; access totab.urlmay still require the user to have granted the site in Safari’s per-site prompt.
Verification
- Open two windows on different sites; open the popup in each and confirm it shows that window’s page.
- Remove
tabsand host permissions; confirmtab.urlis still available in the popup. - Open the popup on a page loaded before install; confirm the injection fallback provides the title.
- 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.
Related
- Opening a popup from the service worker — when the popup is opened programmatically.
- Querying the active tab safely — the query in other contexts.
- Implementing background messaging between popup and service worker — the messages that carry the tab.
- Extension popup architecture — the parent topic.