Chrome Tabs API: Complete Reference Guide
Deep dive into chrome.tabs API — query, create, update, remove, move, group, and event listeners with real TypeScript examples and edge case solutions.
Table of Contents
The chrome.tabs API is the workhorse of Chrome extension development. Almost every extension interacts with tabs in some way — reading URLs, injecting scripts, opening new pages, or reorganizing the user's workspace. Yet the API surface is larger than most developers realize, and the edge cases are numerous.
This reference covers every method you will actually use, with TypeScript examples that handle real-world scenarios — not just the happy path.
| Feature | Method | Permission Needed | Returns | Common Use |
|---|---|---|---|---|
| tabs.query() | None (basic) / tabs (URL) | Tab[] | Find specific tabs | |
| tabs.create() | None | Tab | Open new tabs | |
| tabs.update() | None (basic) / tabs (URL) | Tab | Navigate, pin, mute | |
| tabs.remove() | None | void | Close tabs | |
| tabs.move() | None | Tab | Tab[] | Reorder tabs | |
| tabs.group() | None | number (groupId) | Create tab groups | |
| tabs.sendMessage() | None | any | Talk to content scripts | |
| tabs.captureVisibleTab() | activeTab | string (dataURL) | Screenshot current tab |
tabs.query() — Finding Tabs#
chrome.tabs.query() is the method you will call most often. It filters the browser's tabs based on criteria you provide and returns matching tabs.
tabs.create() — Opening New Tabs#
// Basic — opens a new tab with a URL
const newTab = await chrome.tabs.create({
url: 'https://example.com'
});
// Open next to the current tab (not at the end of the tab strip)
const [currentTab] = await chrome.tabs.query({ active: true, currentWindow: true });
const adjacentTab = await chrome.tabs.create({
url: 'https://example.com',
index: currentTab.index + 1,
openerTabId: currentTab.id // Associates the new tab with the opener
});
// Open without activating (background tab)
const bgTab = await chrome.tabs.create({
url: 'https://example.com',
active: false
});
// Open a pinned tab
const pinnedTab = await chrome.tabs.create({
url: 'https://example.com',
pinned: true
});tabs.update() — Modifying Existing Tabs#
tabs.update() navigates tabs, pins/unpins them, mutes audio, and more. It is the Swiss Army knife for tab manipulation.
// Navigate an existing tab to a new URL
await chrome.tabs.update(tabId, { url: 'https://newsite.com' });
// Pin a tab
await chrome.tabs.update(tabId, { pinned: true });
// Mute a tab
await chrome.tabs.update(tabId, { muted: true });
// Activate (focus) a specific tab
await chrome.tabs.update(tabId, { active: true });
// Multiple updates at once
await chrome.tabs.update(tabId, {
url: 'https://example.com',
pinned: true,
active: true
});tabs.remove() — Closing Tabs#
// Close a single tab
await chrome.tabs.remove(tabId);
// Close multiple tabs at once
await chrome.tabs.remove([tabId1, tabId2, tabId3]);
// Close all tabs matching a pattern (requires 'tabs' permission for URL access)
const oldTabs = await chrome.tabs.query({ url: '*://expired-domain.com/*' });
const tabIds = oldTabs.map(t => t.id).filter((id): id is number => id !== undefined);
await chrome.tabs.remove(tabIds);Closing the last tab in a window closes the window. If you need to prevent this, create a new tab before closing the last one:
async function safeCloseTab(tabId: number, windowId: number) {
const windowTabs = await chrome.tabs.query({ windowId });
if (windowTabs.length === 1) {
// Create a new tab first to prevent window closure
await chrome.tabs.create({ windowId });
}
await chrome.tabs.remove(tabId);
}tabs.move() — Reordering Tabs#
// Move a tab to position 0 (first tab)
await chrome.tabs.move(tabId, { index: 0 });
// Move a tab to the end
await chrome.tabs.move(tabId, { index: -1 });
// Move a tab to a different window
await chrome.tabs.move(tabId, {
windowId: targetWindowId,
index: 0
});
// Move multiple tabs while preserving their relative order
await chrome.tabs.move([tabId1, tabId2, tabId3], { index: 0 });Moving a tab to a different window detaches it from its current window and attaches it to the target. If you move all tabs out of a window, the window closes.
tabs.group() and tabGroups — Tab Group Management#
Tab groups are one of Chrome's most useful organizational features, and extensions can create, modify, and manage them programmatically.
Event Listeners — Reacting to Tab Changes#
The tabs API provides a rich set of events for monitoring tab activity. These run in your service worker.
// Tab created
chrome.tabs.onCreated.addListener((tab) => {
console.log('New tab:', tab.id, tab.pendingUrl || tab.url);
});
// Tab updated (URL change, loading state, title change)
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
// changeInfo only contains properties that actually changed
if (changeInfo.status === 'complete') {
console.log('Tab finished loading:', tabId, tab.url);
}
if (changeInfo.url) {
console.log('Tab navigated to:', changeInfo.url);
}
});
// Tab activated (user switched to a different tab)
chrome.tabs.onActivated.addListener((activeInfo) => {
console.log('Active tab:', activeInfo.tabId, 'in window:', activeInfo.windowId);
});
// Tab closed
chrome.tabs.onRemoved.addListener((tabId, removeInfo) => {
console.log('Tab closed:', tabId);
if (removeInfo.isWindowClosing) {
console.log('Window is closing — all tabs being removed');
}
});
// Tab moved within a window
chrome.tabs.onMoved.addListener((tabId, moveInfo) => {
console.log(`Tab ${tabId} moved from ${moveInfo.fromIndex} to ${moveInfo.toIndex}`);
});
// Tab detached from a window
chrome.tabs.onDetached.addListener((tabId, detachInfo) => {
console.log(`Tab ${tabId} detached from window ${detachInfo.oldWindowId}`);
});
// Tab attached to a window
chrome.tabs.onAttached.addListener((tabId, attachInfo) => {
console.log(`Tab ${tabId} attached to window ${attachInfo.newWindowId}`);
});Filtering onUpdated Events#
onUpdated fires frequently — for every status change, every title update, every favicon load. Without filtering, your handler runs dozens of times per navigation. Use the filter parameter:
// Only listen for URL changes on specific sites
chrome.tabs.onUpdated.addListener(
(tabId, changeInfo, tab) => {
// This only fires when the URL changes on matching tabs
handleGitHubNavigation(tabId, changeInfo.url!);
},
{
properties: ['url'],
urls: ['*://github.com/*'] // Requires 'tabs' permission
}
);tabs.sendMessage() — Communicating with Content Scripts#
// Send a message to a specific tab's content script
try {
const response = await chrome.tabs.sendMessage(tabId, {
action: 'extractData',
selector: '.article-body'
});
console.log('Content script responded:', response);
} catch (err) {
// This throws if no content script is listening in the tab
console.log('No content script in tab:', tabId);
}The receiving content script:
// content-script.ts
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === 'extractData') {
const element = document.querySelector(message.selector);
sendResponse({
text: element?.textContent || null,
html: element?.innerHTML || null
});
}
return true; // Required for async sendResponse
});tabs.captureVisibleTab() — Taking Screenshots#
// Capture the visible area of the active tab as PNG
const dataUrl = await chrome.tabs.captureVisibleTab(
chrome.windows.WINDOW_ID_CURRENT,
{ format: 'png' }
);
// Capture as JPEG with quality setting (0-100)
const jpegUrl = await chrome.tabs.captureVisibleTab(
chrome.windows.WINDOW_ID_CURRENT,
{ format: 'jpeg', quality: 85 }
);
// Use the screenshot
// dataUrl is a base64-encoded data URL you can display in an <img> tag
// or send to a server for processingThis method requires the activeTab permission (or explicit host permission for the tab's URL). It only captures the visible viewport — not the full page. For full-page screenshots, you need to scroll and stitch, or use the debugger API.
Practical Patterns#
Tab Manager: Deduplicate Open Tabs#
async function deduplicateTabs() {
const tabs = await chrome.tabs.query({});
const seen = new Map<string, number>();
const duplicates: number[] = [];
for (const tab of tabs) {
if (!tab.url || !tab.id) continue;
// Normalize URL (remove hash, trailing slash)
const normalized = new URL(tab.url);
normalized.hash = '';
const key = normalized.href.replace(/\/$/, '');
if (seen.has(key)) {
duplicates.push(tab.id);
} else {
seen.set(key, tab.id);
}
}
if (duplicates.length > 0) {
await chrome.tabs.remove(duplicates);
}
return { removed: duplicates.length };
}Workspace Saver: Save and Restore Tab Sessions#
interface Workspace {
name: string;
tabs: { url: string; pinned: boolean; groupTitle?: string }[];
savedAt: number;
}
async function saveWorkspace(name: string): Promise<Workspace> {
const tabs = await chrome.tabs.query({ currentWindow: true });
const groups = await chrome.tabGroups.query({
windowId: chrome.windows.WINDOW_ID_CURRENT
});
const groupMap = new Map(groups.map(g => [g.id, g.title]));
const workspace: Workspace = {
name,
tabs: tabs
.filter(t => t.url && !t.url.startsWith('chrome://'))
.map(t => ({
url: t.url!,
pinned: t.pinned || false,
groupTitle: t.groupId !== -1 ? groupMap.get(t.groupId) || undefined : undefined
})),
savedAt: Date.now()
};
const { workspaces = {} } = await chrome.storage.local.get('workspaces');
workspaces[name] = workspace;
await chrome.storage.local.set({ workspaces });
return workspace;
}
async function restoreWorkspace(name: string): Promise<void> {
const { workspaces = {} } = await chrome.storage.local.get('workspaces');
const workspace: Workspace | undefined = workspaces[name];
if (!workspace) throw new Error(`Workspace '${name}' not found`);
const groupIds = new Map<string, number>();
for (const tabData of workspace.tabs) {
const tab = await chrome.tabs.create({
url: tabData.url,
pinned: tabData.pinned,
active: false
});
if (tabData.groupTitle && tab.id) {
if (!groupIds.has(tabData.groupTitle)) {
const groupId = await chrome.tabs.group({ tabIds: [tab.id] });
await chrome.tabGroups.update(groupId, { title: tabData.groupTitle });
groupIds.set(tabData.groupTitle, groupId);
} else {
await chrome.tabs.group({
tabIds: [tab.id],
groupId: groupIds.get(tabData.groupTitle)!
});
}
}
}
}The chrome.tabs API is stable, well-documented, and essential for nearly every extension. The key pitfalls are permission awareness (many properties require the tabs permission to access), service worker lifecycle management (register event listeners synchronously at the top level), and error handling (tabs can close between when you query them and when you operate on them — always use try-catch). For a deeper understanding of how tabs interact with service workers, see our Background Service Workers Deep Dive.
For related APIs that work alongside tabs, see our Chrome Storage API Complete Guide and Content Scripts Patterns and Pitfalls.
Continue reading
Related articles
Message Passing in Chrome Extensions Explained
Complete guide to message passing in Chrome extensions. Covers one-time messages, long-lived connections, external messaging, and type-safe communication patterns.
Chrome Storage API: The Complete Guide
Everything about Chrome Storage API: local, sync, session, and managed storage. Covers quota limits, migration patterns, type safety, and real-world usage patterns.
Debugging Chrome Extensions Like a Pro
Master Chrome extension debugging with DevTools. Debug service workers, content scripts, popups, storage, and network requests with practical techniques and code examples.