Back
Chapter 8

Detecting Tab Changes

View Source Code

If you remember in the first lesson we briefly mentioned about the background script and we looked into tab detection

But what is background script or service workers

Well they are short-lived JavaScript code that lives in the background so not in the popup and not injected into the webpage

And they execute JavaScript code until they reach the end of their job and then they terminate

How service workers work

Here are a few of the most common ways how you can start an extension service worker

They can listen to browser events such as tab changes downloads or adding a new bookmark

We can also trigger them by the extension lifecycle events such as installing or updating the extension

They can listen to messages that we can programmatically send from the popup or injected code on web pages

They can also be triggered by storage changes or they can listen to context menu events

You can find a list of all the listeners and how to start extension service workers on Google’s Chrome extension reference pages

But enough of the theory let’s get into practice

Setting up tab detection

I open my background.ts file which as you can see already had the tab activated listener

This defineBackground comes from WXT so everything that we put in this function will be added to our final background.ts

This is just how the WXT framework works

So what I want to do is call our changeIcon function here instead of logging which tab has been activated

export default defineBackground(() => {
  browser.tabs.onActivated.addListener(async (activeInfo) => {
    await changeIcon(activeInfo.tabId);
  });
});

ActiveInfo.tabId

Perfect

Now when you switch tabs the icon updates automatically

But wait there’s more

We also need to handle when the URL changes within a tab

So let’s add another listener

export default defineBackground(() => {
  // Tab activated (user switched tabs)
  browser.tabs.onActivated.addListener(async (activeInfo) => {
    await changeIcon(activeInfo.tabId);
  });
  
  // URL changed or page loaded
  browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
    if (changeInfo.url && tab.active) {
      changeIcon(tabId);
    }
  });
});

This handles when you navigate to a new page within the same tab

We check if the URL changed and if the tab is active

Only then do we update the icon

Handling tab closure

What about when you close a tab

When a tab closes the browser activates a different tab

We should update the icon for that newly active tab

export default defineBackground(() => {
  browser.tabs.onActivated.addListener(async (activeInfo) => {
    await changeIcon(activeInfo.tabId);
  });
  
  browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
    if (changeInfo.url && tab.active) {
      changeIcon(tabId);
    }
  });
  
  // Tab closed (update newly active tab)
  browser.tabs.onRemoved.addListener(async () => {
    const [tab] = await browser.tabs.query({ 
      active: true, 
      currentWindow: true 
    });
    
    if (tab?.id) {
      await changeIcon(tab.id);
    }
  });
});

Perfect

Now we handle all the cases

Switching tabs navigating to new URLs closing tabs

The icon updates correctly in every scenario

Organizing the code

Now we’re calling changeIcon from multiple places

It makes sense to move it to a utility file so we can reuse it

// utils/changeIcon.ts
import { storage } from 'wxt/storage';
import getPageKey from './getPageKey';

const changeIcon = async (tabId: number) => {
  const tab = await browser.tabs.get(tabId);
  if (!tab.url) return;
  
  const pageKey = getPageKey(tab.url);
  const notesStorage = await storage.getItem('sync:notesStorage');
  const noteData = notesStorage?.[pageKey];
  
  if (noteData) {
    await browser.action.setIcon({
      tabId,
      path: {
        "16": "icon-full/16.png",
        "32": "icon-full/32.png",
        "48": "icon-full/48.png",
        "96": "icon-full/96.png",
        "128": "icon-full/128.png"
      }
    });
  } else {
    await browser.action.setIcon({
      tabId,
      path: {
        "16": "icon/16.png",
        "32": "icon/32.png",
        "48": "icon/48.png",
        "96": "icon/96.png",
        "128": "icon/128.png"
      }
    });
  }
};

export default changeIcon;

Now import it in both background and popup

import changeIcon from '@/utils/changeIcon';

What’s next

With tab detection working your extension now provides real-time visual feedback as users browse the web

The icon updates automatically when you switch tabs navigate to new pages or close tabs

In the next chapter we’ll learn about content scripts and how to inject code into web pages