Development13 min read

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.

C
CWS Kit Team
Share

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.

FeatureMethodPermission NeededReturnsCommon Use
tabs.query()None (basic) / tabs (URL)Tab[]Find specific tabs
tabs.create()NoneTabOpen new tabs
tabs.update()None (basic) / tabs (URL)TabNavigate, pin, mute
tabs.remove()NonevoidClose tabs
tabs.move()NoneTab | Tab[]Reorder tabs
tabs.group()Nonenumber (groupId)Create tab groups
tabs.sendMessage()NoneanyTalk to content scripts
tabs.captureVisibleTab()activeTabstring (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.

```typescript // Get the currently active tab in the current window const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); console.log(tab.id); // Always available console.log(tab.index); // Position in tab strip console.log(tab.url); // ONLY available with 'tabs' permission console.log(tab.title); // ONLY available with 'tabs' permission ``` Without the `tabs` permission, `url` and `title` return `undefined`. You get the tab ID, index, window ID, and status — enough for most operations that don't need to read the page address.

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.

```typescript // Group specific tabs const groupId = await chrome.tabs.group({ tabIds: [tabId1, tabId2, tabId3] }); // Customize the group appearance await chrome.tabGroups.update(groupId, { title: 'Research', color: 'blue' // grey, blue, red, yellow, green, pink, purple, cyan, orange }); // Group tabs in a specific window const groupId2 = await chrome.tabs.group({ tabIds: [tabId4, tabId5], createProperties: { windowId: targetWindowId } }); ```

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 processing

This 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)!
        });
      }
    }
  }
}
Tabs API Summary

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

View all posts