How to Get the Current Tab URL in a Chrome Extension (MV3)
Learn how to get the current tab URL in a Chrome extension using Manifest V3, activeTab, tabs API, service workers, popups, and content scripts.
To make your Chrome extension get the current URL, you must match the right API to your execution context. Calling the wrong method or missing a permission will return undefined and break your feature.
To get the current tab URL, call chrome.tabs.query({ active: true, currentWindow: true }) from a popup or service worker, then read the tab.url property. Your manifest.json must include the activeTab or tabs permission. If your code runs inside a content script, skip the Tabs API entirely and read window.location.href directly.
Which API Should You Use?
Always verify where your code runs before querying the tab. Use currentWindow for popups, lastFocusedWindow for service workers, and window.location for content scripts.
1. When Chrome Already Provides the Tab Object
If your logic fires from an action click, keyboard command, or context menu, Chrome passes the active Tab object directly into the callback. Do not call chrome.tabs.query().
// action.onClicked
chrome.action.onClicked.addListener((tab) => {
console.log("Current URL:", tab.url);
});
// commands.onCommand
chrome.commands.onCommand.addListener((command, tab) => {
console.log(`Command: ${command}, URL: ${tab?.url}`);
});
// contextMenus.onClicked
chrome.contextMenus.onClicked.addListener((info, tab) => {
console.log("Clicked on URL:", tab?.url);
});Note: action.onClicked does not fire if your extension uses a default popup.
2. Getting the Current URL in a Popup Script
Popups are extension pages attached to a specific browser window. Pass currentWindow: true to target the exact window where the user opened the popup.
// popup.js
async function getPopupUrl() {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
return tab?.url;
}Requires "permissions": ["activeTab"] in manifest.json.
3. Getting the Current URL in a Service Worker (Background Script)
Service workers run invisibly in the background, detached from browser windows. Passing currentWindow: true behaves unpredictably here. Use lastFocusedWindow: true instead.
// background.js
async function getBackgroundUrl() {
const [tab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
return tab?.url;
}4. Getting the URL in a Content Script
Content scripts run directly inside the webpage DOM. They cannot access the chrome.tabs API. To have your Chrome extension get the URL in a content script, read it directly from the window object.
// content.js
const currentUrl = window.location.href;
chrome.runtime.sendMessage({ type: "SEND_URL", url: currentUrl });5. Getting the Current URL in a Side Panel
Side panels persist across tab switches. Query the URL on the initial load, then use chrome.tabs.onActivated and chrome.tabs.onUpdated listeners to keep the UI synced as the user navigates.
Chrome Extension Permissions: activeTab vs. tabs
Default to least privilege. Use activeTab for user-invoked actions to avoid scary installation warnings. Use tabs only for persistent background monitoring.
activeTab: Grants temporary access to the active tab's sensitive properties (url,title,favIconUrl) only after a deliberate user gesture (like clicking the extension icon). It does not trigger a privacy warning during installation.tabs: Grants permanent access to sensitive fields across all tabs. This triggers a broad "Read your browsing history" install warning. Chrome's declare permissions guidance notes that users are more likely to trust extensions with limited warnings or when permissions are explained to them. Use this only for dedicated tab managers.- Host Permissions: If you only need the URL on a specific site (for example,
*://*.github.com/*), declare that pattern. It unlocks the URL for matching sites without requiring the globaltabspermission.
The tabs Permission Misconception
The tabs permission does not unlock the entire chrome.tabs API. It only unlocks four sensitive string properties on the Tab object: url, pendingUrl, title, and favIconUrl. You can freely query tab IDs, statuses, or window IDs without it.
Handling URL Changes and Loading States
Reading the URL once works for popups. Persistent scripts must use onActivated (for tab switches) and onUpdated (for navigation) to stay accurate.
chrome.tabs.onActivated: Fires when the user switches tabs. The new tab's URL may not be fully initialized when this event triggers.chrome.tabs.onUpdated: Fires when a tab's URL commits or updates. This is the safest place to execute domain-specific logic.- Loading States: Newly created tabs often lack a committed
url. Always checkpendingUrlas a fallback.
const currentUrl = tab.url ?? tab.pendingUrl;Troubleshooting: Why is tab.url Undefined?
Do not blindly add the tabs permission to fix an undefined URL. Identify if the context, query scope, or loading state is the actual culprit.
If your Chrome extension gets the current URL as undefined, check these common failure points:
- Missing Permissions: Querying a background tab asynchronously without the
tabsor host permission returns an undefined URL. - Wrong Execution Context: Calling
chrome.tabsinside a content script throws an error. - Empty Query Array: Queries originating from DevTools or background contexts without active windows can return an empty array (
tabs[0]is undefined). Always null-check:if (!tab) return;. - Misusing
getCurrent():chrome.tabs.getCurrent()returns the tab hosting the script itself. Calling it from a popup or service worker returnsundefinedbecause they are not browsing tabs. - Unresolved Promises:
chrome.tabs.queryis asynchronous. If you log an[object Promise], addawait.
Edge Cases to Test Before Shipping
Standard testing on standard web pages is not enough. Verify how your extension behaves on restricted Chrome pages, local files, and multi-window setups.
- Restricted Pages: Chrome blocks access to
chrome://URLs and the Chrome Web Store. - Local Files & Incognito: Extensions cannot read
file://URLs or run in incognito mode by default. Users must manually allow this in the extension's management settings. Usechrome.extension.isAllowedFileSchemeAccess()to verify. - Frozen Tabs: A discarded or frozen tab (Chrome 132+) stays in memory but pauses execution.
- Multiple Windows: If two Chrome windows are open, querying
{ active: true }without specifying the window returns two tabs. Always limit scope withcurrentWindoworlastFocusedWindow.
FAQ
Should I use currentWindow or lastFocusedWindow?
Use currentWindow: true in popup scripts attached to a specific UI window. Use lastFocusedWindow: true in service workers to avoid detached-window logic bugs.
Can I get the URL by tab ID?
Yes. If you have the tabId, call await chrome.tabs.get(tabId). The same permission rules dictate whether the url property is visible.
Does chrome.runtime.getURL() return the active page URL?
No. chrome.runtime.getURL() returns the internal file path of an asset bundled inside your extension directory (for example, your icon image), not the active web address.
Next Steps for a Production-Ready Extension
Once your Chrome extension can successfully get the current URL, harden the codebase:
- Remove unused permissions to reduce install friction.
- Test popup, service worker, and content script flows independently.
- Add null checks for
tab.urlandtab.pendingUrl.
Monetization Considerations
If you plan to monetize a stable extension, prioritize transparent, opt-in models. Tools like Mellowtel allow users to support your development by voluntarily sharing a fraction of their unused internet bandwidth. It operates entirely on explicit user consent, with clear opt-in and opt-out flows managed via generateAndOpenOptInLink(). Keep permission requests strictly justifiable, and ensure your consent settings remain easily accessible to users.