Tutorials

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.

M

Mellowtel

4 min read

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 global tabs permission.

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 check pendingUrl as 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:

  1. Missing Permissions: Querying a background tab asynchronously without the tabs or host permission returns an undefined URL.
  2. Wrong Execution Context: Calling chrome.tabs inside a content script throws an error.
  3. 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;.
  4. Misusing getCurrent(): chrome.tabs.getCurrent() returns the tab hosting the script itself. Calling it from a popup or service worker returns undefined because they are not browsing tabs.
  5. Unresolved Promises: chrome.tabs.query is asynchronous. If you log an [object Promise], add await.

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. Use chrome.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 with currentWindow or lastFocusedWindow.

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:

  1. Remove unused permissions to reduce install friction.
  2. Test popup, service worker, and content script flows independently.
  3. Add null checks for tab.url and tab.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.

On this page