Working with Tab Groups in Chrome Extensions — Developer Guide

31 min read

Working with Tab Groups in Chrome Extensions

Overview

The Chrome Tab Groups API (chrome.tabGroups) is an essential tool for building productivity-focused browser extensions. Introduced in Chrome 88, this API enables extensions to programmatically create, organize, and manage tab groups—allowing users to visually organize their browser workspace into color-coded categories. Whether you’re building a tab manager, a project-based organization tool, or a workflow automation extension, the Tab Groups API provides the foundation for helping users tame tab overload.

This guide covers the complete Tab Groups API: creating and managing groups, customizing group colors and titles, moving tabs between groups, collapsing and expanding groups, listening for group events, and implementing common productivity extension patterns.

Prerequisites

Before using the Tab Groups API, add the required permissions to your manifest.json:

{
  "manifest_version": 3,
  "name": "My Tab Group Extension",
  "version": "1.0",
  "permissions": ["tabGroups", "tabs"]
}

The tabGroups permission is required for all group operations (create, read, update, delete). The tabs permission is needed for accessing tab URLs, titles, and for implementing grouping logic based on domain, project, or other criteria.

Browser Support:

Understanding the TabGroup Object

The TabGroup object represents a group of tabs and contains these properties:

Property Type Description
id number Unique group identifier assigned by Chrome
title string Display name of the group (up to 50 characters)
color string Group color from a predefined palette
collapsed boolean Whether the group is currently collapsed
windowId number ID of the parent window containing this group

Available Colors

Tab groups support nine predefined colors:

type TabGroupColor = 
  | 'grey'    // Gray
  | 'blue'    // Blue
  | 'red'     // Red
  | 'yellow'  // Yellow
  | 'green'   // Green
  | 'pink'    // Pink
  | 'purple'  // Purple
  | 'cyan'    // Cyan
  | 'orange'; // Orange

Creating Tab Groups

Tab groups are created implicitly by grouping existing tabs using the chrome.tabs.group() method. This is the primary and only way to create new groups.

Basic Group Creation

// Group multiple tabs into a new group
const tabIds = [tab1.id, tab2.id, tab3.id];
const groupId = await chrome.tabs.group({ tabIds });

// The group is automatically created with default title and color
console.log(`Created group with ID: ${groupId}`);

Creating a Group with Custom Properties

// Group tabs and immediately set title and color
async function createProjectGroup(tabIds: number[], projectName: string, color: string) {
  const groupId = await chrome.tabs.group({ tabIds });
  
  await chrome.tabGroups.update(groupId, {
    title: projectName,
    color: color
  });
  
  return groupId;
}

// Usage
const developmentTabs = await chrome.tabs.query({ url: "*://localhost/*" });
await createProjectGroup(
  developmentTabs.map(t => t.id), 
  "Dev Environment", 
  "green"
);

Creating a Group from Active Tab Context

// Create a group from the currently active tab and related tabs
async function createGroupFromActive(relatedTabIds: number[]) {
  const [activeTab] = await chrome.tabs.query({ 
    active: true, 
    currentWindow: true 
  });
  
  if (!activeTab) return null;
  
  const allTabIds = [activeTab.id, ...relatedTabIds];
  const groupId = await chrome.tabs.group({ tabIds: allTabIds });
  
  await chrome.tabGroups.update(groupId, {
    title: "New Group",
    color: "blue"
  });
  
  return groupId;
}

Managing Groups

Updating Group Properties

// Update group title
await chrome.tabGroups.update(groupId, {
  title: "Updated Project Name"
});

// Update group color
await chrome.tabGroups.update(groupId, {
  color: "purple"
});

// Update both title and color
await chrome.tabGroups.update(groupId, {
  title: "Research",
  color: "cyan"
});

Getting Group Information

// Get a specific group by ID
const group = await chrome.tabGroups.get(groupId);
console.log(group.title, group.color, group.collapsed);

// Get all groups in the current window
async function getAllGroupsInWindow(windowId: number) {
  const tabs = await chrome.tabs.query({ windowId });
  
  // Extract unique group IDs from tabs
  const groupIds = [...new Set(
    tabs
      .filter(t => t.groupId !== -1)
      .map(t => t.groupId)
  )];
  
  const groups = await Promise.all(
    groupIds.map(id => chrome.tabGroups.get(id))
  );
  
  return groups;
}

// Usage
const windowId = (await chrome.windows.getCurrent()).id;
const groups = await getAllGroupsInWindow(windowId);
console.log(`Found ${groups.length} groups`);

Deleting Groups

// Delete a group (tabs remain in the window, just ungrouped)
await chrome.tabGroups.remove(groupId);

// Delete all groups in the current window
async function clearAllGroups() {
  const tabs = await chrome.tabs.query({ currentWindow: true });
  const groupIds = [...new Set(tabs.map(t => t.groupId).filter(id => id !== -1))];
  
  for (const groupId of groupIds) {
    await chrome.tabGroups.remove(groupId);
  }
}

Moving Tabs Between Groups

Adding Tabs to a Group

// Add a single tab to an existing group
await chrome.tabs.group({ tabIds: [tabId], groupId: existingGroupId });

// Add multiple tabs to a group
const newTabIds = [tabId1, tabId2, tabId3];
await chrome.tabs.group({ tabIds: newTabIds, groupId: existingGroupId });

Removing Tabs from Groups

// Remove a tab from its group (becomes ungrouped)
await chrome.tabs.ungroup([tabId]);

// Remove multiple tabs from their groups
await chrome.tabs.ungroup([tabId1, tabId2, tabId3]);

// Move all tabs from one group to another
async function mergeGroups(sourceGroupId: number, targetGroupId: number) {
  const tabs = await chrome.tabs.query({});
  const sourceTabs = tabs.filter(t => t.groupId === sourceGroupId);
  
  // Add all tabs from source to target
  await chrome.tabs.group({
    tabIds: sourceTabs.map(t => t.id),
    groupId: targetGroupId
  });
  
  // Remove the empty source group
  await chrome.tabGroups.remove(sourceGroupId);
}

Moving Tabs Between Groups

// Move a tab from one group to another
async function moveTabToGroup(tabId: number, targetGroupId: number) {
  // First, ungroup the tab
  await chrome.tabs.ungroup([tabId]);
  
  // Then add it to the target group
  await chrome.tabs.group({ tabIds: [tabId], groupId: targetGroupId });
}

// Move a tab to a new group (creates new group automatically)
async function moveTabToNewGroup(tabId: number, groupTitle: string, color: string) {
  // Remove from current group first
  await chrome.tabs.ungroup([tabId]);
  
  // Create new group with the tab
  const groupId = await chrome.tabs.group({ tabIds: [tabId] });
  
  // Set properties
  await chrome.tabGroups.update(groupId, { title: groupTitle, color });
  
  return groupId;
}

Collapsing and Expanding Groups

Tab groups can be collapsed to hide all their tabs, providing a cleaner tab strip interface.

Collapsing Groups

// Collapse a group
await chrome.tabGroups.update(groupId, { collapsed: true });

// Collapse all groups in the window
async function collapseAllGroups() {
  const tabs = await chrome.tabs.query({ currentWindow: true });
  const groupIds = [...new Set(tabs.map(t => t.groupId).filter(id => id !== -1))];
  
  for (const groupId of groupIds) {
    await chrome.tabGroups.update(groupId, { collapsed: true });
  }
}

Expanding Groups

// Expand a group
await chrome.tabGroups.update(groupId, { collapsed: false });

// Expand a specific group and collapse all others
async function expandOnlyGroup(targetGroupId: number) {
  const tabs = await chrome.tabs.query({ currentWindow: true });
  const groupIds = [...new Set(tabs.map(t => t.groupId).filter(id => id !== -1))];
  
  for (const groupId of groupIds) {
    await chrome.tabGroups.update(groupId, { 
      collapsed: groupId !== targetGroupId 
    });
  }
}

Toggle Group Collapse State

// Toggle collapse state
async function toggleGroupCollapse(groupId: number) {
  const group = await chrome.tabGroups.get(groupId);
  await chrome.tabGroups.update(groupId, { 
    collapsed: !group.collapsed 
  });
}

Working with Group Events

The Tab Groups API provides events for monitoring group lifecycle changes.

Listening for Group Creation

// Listen for new group creation
chrome.tabGroups.onCreated.addListener((group) => {
  console.log(`Group created: ${group.title} (${group.color})`);
  
  // Could trigger UI updates, logging, etc.
});

Listening for Group Updates

// Listen for group property changes (title, color, collapse state)
chrome.tabGroups.onUpdated.addListener((group) => {
  console.log(`Group updated: ${group.title}`);
  console.log(`  Color: ${group.color}`);
  console.log(`  Collapsed: ${group.collapsed}`);
});

Listening for Group Removal

// Listen for group deletion
chrome.tabGroups.onRemoved.addListener((groupId) => {
  console.log(`Group removed: ${groupId}`);
  
  // Clean up any stored references
  groupMetadata.delete(groupId);
});

Listening for Tab-Group Associations

// When tabs are grouped or ungrouped
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
  if (changeInfo.groupId !== undefined) {
    if (changeInfo.groupId === -1) {
      console.log(`Tab ${tabId} was ungrouped`);
    } else {
      console.log(`Tab ${tabId} was added to group ${changeInfo.groupId}`);
    }
  }
});

Organizing Tabs Programmatically

Auto-Group by Domain

// Automatically group tabs by domain
async function groupTabsByDomain() {
  const tabs = await chrome.tabs.query({ currentWindow: true });
  
  // Group tabs by domain
  const domainGroups = new Map<string, number[]>();
  
  for (const tab of tabs) {
    if (!tab.url || tab.url.startsWith('chrome://')) continue;
    
    try {
      const url = new URL(tab.url);
      const domain = url.hostname;
      
      if (!domainGroups.has(domain)) {
        domainGroups.set(domain, []);
      }
      domainGroups.get(domain)!.push(tab.id);
    } catch (e) {
      // Invalid URL, skip
    }
  }
  
  // Create groups for each domain
  const colors: TabGroupColor[] = ['blue', 'green', 'red', 'yellow', 'purple', 'cyan', 'orange', 'pink', 'grey'];
  let colorIndex = 0;
  
  for (const [domain, tabIds] of domainGroups) {
    if (tabIds.length < 2) continue; // Skip single tabs
    
    const groupId = await chrome.tabs.group({ tabIds });
    await chrome.tabGroups.update(groupId, {
      title: domain,
      color: colors[colorIndex % colors.length]
    });
    
    colorIndex++;
  }
}

Group by Time-Based Organization

// Group tabs opened in the same session/time period
async function groupTabsByAge() {
  const tabs = await chrome.tabs.query({ currentWindow: true });
  
  const now = Date.now();
  const oneHour = 60 * 60 * 1000;
  const oneDay = 24 * oneHour;
  
  const groups: { [key: string]: number[] } = {
    'Recent (last hour)': [],
    'Today': [],
    'Older': []
  };
  
  for (const tab of tabs) {
    if (!tab.id || tab.groupId !== -1) continue; // Skip already grouped
    
    // Note: There's no direct "opened time" in tab object
    // This is a simplified example - you'd need to track tab creation time
    // through storage or other means
    groups['Older'].push(tab.id);
  }
  
  // Create time-based groups
  for (const [label, tabIds] of Object.entries(groups)) {
    if (tabIds.length === 0) continue;
    
    const groupId = await chrome.tabs.group({ tabIds });
    await chrome.tabGroups.update(groupId, {
      title: label,
      color: label === 'Recent (last hour)' ? 'green' : 
             label === 'Today' ? 'blue' : 'grey'
    });
  }
}

Intelligent Tab Grouping

// Smart grouping based on URL patterns and content
interface GroupRule {
  name: string;
  color: TabGroupColor;
  patterns: string[];
}

const groupRules: GroupRule[] = [
  {
    name: "Social Media",
    color: "blue",
    patterns: ["*://*.twitter.com/*", "*://*.facebook.com/*", "*://*.reddit.com/*"]
  },
  {
    name: "Development",
    color: "green",
    patterns: ["*://*.github.com/*", "*://*.stackoverflow.com/*", "*://localhost/*"]
  },
  {
    name: "Documentation",
    color: "yellow",
    patterns: ["*://*.mdn.io/*", "*://*.docs.google.com/*"]
  }
];

async function applyIntelligentGrouping() {
  const tabs = await chrome.tabs.query({ currentWindow: true });
  
  for (const rule of groupRules) {
    const matchingTabs = [];
    
    for (const tab of tabs) {
      if (!tab.url || tab.groupId !== -1) continue;
      
      for (const pattern of rule.patterns) {
        if (await matchesUrl(tab.url, pattern)) {
          matchingTabs.push(tab.id);
          break;
        }
      }
    }
    
    if (matchingTabs.length > 0) {
      const groupId = await chrome.tabs.group({ tabIds: matchingTabs });
      await chrome.tabGroups.update(groupId, {
        title: rule.name,
        color: rule.color
      });
    }
  }
}

async function matchesUrl(url: string, pattern: string): Promise<boolean> {
  // Simple pattern matching - in production use chrome.matchPatterns
  if (pattern.includes('*://')) {
    const regex = new RegExp(pattern
      .replace(/\./g, '\\.')
      .replace(/\*/g, '.*')
      .replace(/\?/g, '\\?')
    );
    return regex.test(url);
  }
  return false;
}

Productivity Extension Patterns

Tab Group Manager UI

// Example: Managing tab groups from a popup UI

interface GroupData {
  id: number;
  title: string;
  color: string;
  collapsed: boolean;
  tabCount: number;
}

// Get all groups with tab counts
async function getAllGroups(): Promise<GroupData[]> {
  const tabs = await chrome.tabs.query({ currentWindow: true });
  
  const groupMap = new Map<number, GroupData>();
  
  for (const tab of tabs) {
    if (tab.groupId === -1) continue;
    
    if (!groupMap.has(tab.groupId)) {
      const group = await chrome.tabGroups.get(tab.groupId);
      groupMap.set(tab.groupId, {
        id: group.id,
        title: group.title,
        color: group.color,
        collapsed: group.collapsed,
        tabCount: 0
      });
    }
    groupMap.get(tab.groupId)!.tabCount++;
  }
  
  return Array.from(groupMap.values());
}

// Create new group from popup
async function createGroupFromPopup(title: string, color: string) {
  const [activeTab] = await chrome.tabs.query({ 
    active: true, 
    currentWindow: true 
  });
  
  if (!activeTab) return null;
  
  const groupId = await chrome.tabs.group({ tabIds: [activeTab.id] });
  await chrome.tabGroups.update(groupId, { title, color });
  
  return groupId;
}

Keyboard Shortcut Integration

// Example: Using commands API with tab groups

// manifest.json
/*
{
  "commands": {
    "group-selected-tabs": {
      "suggested_key": {
        "default": "Ctrl+Shift+G",
        "mac": "Command+Shift+G"
      },
      "description": "Group selected tabs"
    }
  }
}
*/

// background.ts
chrome.commands.onCommand.addListener(async (command) => {
  if (command === 'group-selected-tabs') {
    const tabs = await chrome.tabs.query({ 
      currentWindow: true, 
      highlighted: true 
    });
    
    if (tabs.length > 1) {
      const groupId = await chrome.tabs.group({ 
        tabIds: tabs.map(t => t.id) 
      });
      await chrome.tabGroups.update(groupId, {
        title: 'Quick Group',
        color: 'blue'
      });
    }
  }
});

Saving and Restoring Group Layouts

// Save group layout to storage
async function saveGroupLayout(): Promise<string> {
  const tabs = await chrome.tabs.query({ currentWindow: true });
  
  const layout = {
    timestamp: Date.now(),
    groups: [] as any[]
  };
  
  // Group tabs by groupId
  const tabGroups = new Map<number, any[]>();
  for (const tab of tabs) {
    if (tab.groupId === -1) continue;
    if (!tabGroups.has(tab.groupId)) {
      tabGroups.set(tab.groupId, []);
    }
    tabGroups.get(tab.groupId)!.push({
      url: tab.url,
      title: tab.title,
      pinned: tab.pinned
    });
  }
  
  // Get group info
  for (const [groupId, groupTabs] of tabGroups) {
    const group = await chrome.tabGroups.get(groupId);
    layout.groups.push({
      title: group.title,
      color: group.color,
      collapsed: group.collapsed,
      tabs: groupTabs
    });
  }
  
  return JSON.stringify(layout);
}

// Restore group layout from storage
async function restoreGroupLayout(layoutJson: string) {
  const layout = JSON.parse(layoutJson);
  
  for (const groupData of layout.groups) {
    const tabIds: number[] = [];
    
    for (const tabData of groupData.tabs) {
      const newTab = await chrome.tabs.create({
        url: tabData.url,
        pinned: tabData.pinned,
        active: false
      });
      tabIds.push(newTab.id);
    }
    
    if (tabIds.length > 0) {
      const groupId = await chrome.tabs.group({ tabIds });
      await chrome.tabGroups.update(groupId, {
        title: groupData.title,
        color: groupData.color,
        collapsed: groupData.collapsed
      });
    }
  }
}

Context Menu Integration

// Add context menu for tab grouping

// Create context menu items
chrome.contextMenus.create({
  id: 'group-tabs',
  title: 'Group tabs from this domain',
  contexts: ['page', 'tab']
});

chrome.contextMenus.create({
  id: 'add-to-group',
  title: 'Add to group',
  contexts: ['page', 'tab'],
  // Dynamic menu items would be added based on existing groups
});

chrome.contextMenus.create({
  id: 'ungroup-tabs',
  title: 'Remove from group',
  contexts: ['page', 'tab']
});

// Handle context menu clicks
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
  if (!tab) return;
  
  switch (info.menuItemId) {
    case 'group-tabs':
      // Group all tabs from the same domain
      const tabs = await chrome.tabs.query({ 
        currentWindow: true 
      });
      const domain = new URL(tab.url!).hostname;
      const domainTabs = tabs.filter(t => 
        t.url && new URL(t.url).hostname === domain
      );
      
      if (domainTabs.length > 1) {
        const groupId = await chrome.tabs.group({ 
          tabIds: domainTabs.map(t => t.id) 
        });
        await chrome.tabGroups.update(groupId, {
          title: domain,
          color: 'blue'
        });
      }
      break;
      
    case 'ungroup-tabs':
      await chrome.tabs.ungroup([tab.id]);
      break;
  }
});

Best Practices

1. Always Check Group Existence

// ❌ Bad: May fail if group doesn't exist
await chrome.tabGroups.update(groupId, { title: "New Title" });

// ✅ Good: Verify group exists first
async function safeUpdateGroup(groupId: number, updates: any) {
  try {
    const group = await chrome.tabGroups.get(groupId);
    await chrome.tabGroups.update(groupId, updates);
  } catch (e) {
    console.error('Group not found:', groupId);
  }
}

2. Handle Tab IDs Carefully

// ❌ Bad: Tab IDs can become invalid after closure
const tabId = tabs[0].id;
await chrome.tabs.group({ tabIds: [tabId] }); // May fail

// ✅ Good: Validate tab IDs before use
const validTabIds = (await chrome.tabs.query({})).map(t => t.id);
const existingIds = tabIds.filter(id => validTabIds.includes(id));

if (existingIds.length > 0) {
  await chrome.tabs.group({ tabIds: existingIds });
}

3. Debounce Group Operations

// When auto-grouping, debounce to avoid excessive operations
let groupDebounceTimer: number;

async function debouncedAutoGroup() {
  clearTimeout(groupDebounceTimer);
  groupDebounceTimer = setTimeout(async () => {
    await groupTabsByDomain();
  }, 1000) as any;
}

chrome.tabs.onUpdated.addListener((tabId, changeInfo) => {
  if (changeInfo.url) {
    debouncedAutoGroup();
  }
});

4. Clean Up Event Listeners

// In service workers, be mindful of event listener lifecycle
let groupCreatedListener: ((group: any) => void) | null = null;

function setupGroupListeners() {
  if (groupCreatedListener) return;
  
  groupCreatedListener = (group) => {
    console.log('Group created:', group.title);
  };
  
  chrome.tabGroups.onCreated.addListener(groupCreatedListener);
}

function cleanupGroupListeners() {
  if (groupCreatedListener) {
    chrome.tabGroups.onCreated.removeListener(groupCreatedListener);
    groupCreatedListener = null;
  }
}

5. Consider User Experience

// ❌ Bad: Automatically reorganizing user's tabs without indication
async function autoGroupAll() {
  const tabs = await chrome.tabs.query({ currentWindow: true });
  // ... group everything immediately
}

// ✅ Good: Provide user feedback and controls
async function smartGroupWithFeedback() {
  // Show notification that grouping is happening
  chrome.runtime.sendMessage({
    type: 'GROUPING_STARTED',
    message: 'Organizing your tabs...'
  });
  
  // Perform grouping
  await groupTabsByDomain();
  
  // Notify completion
  chrome.runtime.sendMessage({
    type: 'GROUPING_COMPLETE',
    message: 'Tabs organized by domain'
  });
}

API Reference Summary

Methods

Method Description
chrome.tabGroups.get(groupId) Get group details by ID
chrome.tabGroups.update(groupId, properties) Update group title, color, or collapse state
chrome.tabGroups.remove(groupId) Delete a group (tabs remain)
chrome.tabs.group(options) Create a group from tabs
chrome.tabs.ungroup(tabIds) Remove tabs from their groups

Properties

Property Type Description
group.id number Unique group identifier
group.title string Display name (max 50 chars)
group.color string Group color
group.collapsed boolean Collapse state
group.windowId number Parent window ID

Events

Event Description
chrome.tabGroups.onCreated New group created
chrome.tabGroups.onUpdated Group properties changed
chrome.tabGroups.onRemoved Group deleted
chrome.tabs.onUpdated Fires with groupId property when tab’s group changes

Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.

No previous article
No next article