Showing Different Side Panel Content per Tab

Scope the Chrome side panel to specific tabs or sites — sidePanel.setOptions with tabId, enabling it only where it applies, per-tab paths, and keeping panel state in sync as the user switches tabs.

Published September 18, 2026 Updated September 18, 2026 7 min read
Table of Contents

By default the side panel is global: one panel, the same document, visible whichever tab is active. That is right for a notes tool and wrong for a panel about the current page — a page summary, a code review helper, a shopping comparison. For those, the panel should follow the tab: different content per tab, or absent entirely where it does not apply. chrome.sidePanel.setOptions with a tabId makes this possible, with a few behaviours that are not obvious from the API reference. This guide is part of side panel and DevTools interfaces.

Global and per-tab panels

Global versus tab-scoped side panelsA global panel and a tab-scoped panel compared on what happens when switching tabs, whether the document persists, how enablement is controlled and the typical use.BehaviourGlobal panelTab-scoped panelSwitching tabsSame panel staysPanel follows the tabPanel documentOne, long-livedPer tab, recreatedHide on some pagesNot per pageenabled: false per tabDifferent path per tabNopath per tabIdTypical useNotes, chat, reading listAbout this page
A tab-scoped panel is torn down and recreated as the user switches — design its state for that.

Step-by-step

1. Disable the global panel

1{
2  "permissions": ["sidePanel", "tabs"],
3  "side_panel": { "default_path": "panel.html" }
4}
1chrome.runtime.onInstalled.addListener(() => {
2  chrome.sidePanel.setOptions({ enabled: false });          // off unless a tab turns it on
3});

Execution context: the service worker. Disabling the global default means the panel exists only where you explicitly enable it for a tab. The tabs permission is needed to read tab.url in the listeners below; if the decision can be made from activeTab at click time instead, you can avoid it.

2. Enable it per tab where it applies

 1const SUPPORTED = /^https:\/\/(github\.com|gitlab\.com)\//;
 2
 3async function syncPanelForTab(tabId, url) {
 4  if (url && SUPPORTED.test(url)) {
 5    await chrome.sidePanel.setOptions({ tabId, path: "panel.html", enabled: true });
 6  } else {
 7    await chrome.sidePanel.setOptions({ tabId, enabled: false });
 8  }
 9}
10
11chrome.tabs.onUpdated.addListener((tabId, info, tab) => {
12  if (info.url || info.status === "loading") syncPanelForTab(tabId, tab.url);
13});

Execution context: the service worker, with the listener at the top level. With enabled: false for a tab, an open panel closes when the user switches to it and reappears when they switch back to a supported tab. Updating on navigation within a tab — a user moving from GitHub to a news site in the same tab — is what makes the panel disappear at the right moment.

3. Give different tabs different documents

1async function panelFor(tabId, url) {
2  const u = new URL(url);
3  const path = u.pathname.includes("/pull/") ? "panel.html#review" : "panel.html#repo";
4  await chrome.sidePanel.setOptions({ tabId, path, enabled: true });
5}

Execution context: the service worker. Distinct path values produce distinct panel documents per tab. Using a fragment on one HTML file keeps a single bundle; separate HTML files are also fine when the panels share little.

4. Tell the panel which tab it belongs to

A tab-scoped panel document does not receive its tab id as a parameter. Put it in the path.

1await chrome.sidePanel.setOptions({ tabId, path: `panel.html?tab=${tabId}`, enabled: true });
1// panel.js
2const tabId = Number(new URLSearchParams(location.search).get("tab"));
3const tab = await chrome.tabs.get(tabId);
4render(await summarise(tab.url));

Execution context: the service worker sets the path; the panel document reads it. Without this, the panel must guess “the active tab”, which is wrong the moment the user switches tabs while the panel is still loading.

5. Persist per-tab state outside the panel

A tab-scoped panel’s document is discarded when the user switches away and recreated when they return. Anything the user did in it — a scroll position, a draft comment — must survive that.

1// panel.js
2const KEY = `panel:${tabId}`;
3const { [KEY]: saved = {} } = await chrome.storage.session.get(KEY);
4draft.value = saved.draft ?? "";
5list.scrollTop = saved.scroll ?? 0;
6
7addEventListener("pagehide", () => {
8  chrome.storage.session.set({ [KEY]: { draft: draft.value, scroll: list.scrollTop } });
9});

Execution context: the side panel document. storage.session keyed by tab id is the natural home: it survives the document being recreated and is cleared with the browser session, when tab ids stop meaning anything. Clean up the key on tabs.onRemoved so it does not accumulate.

Switching between a supported and an unsupported tabOn a supported tab the panel is enabled with a tab-specific path; switching to an unsupported tab hides it; switching back recreates the panel, which restores its state from session storage.UserService workerSide panelstorage.sessionfocus tab 12 (github.com)setOptions({tabId:12, path, enabled})pagehide → save draftuser switches awayfocus tab 15 (news site)enabled: false → panel hiddenback to tab 12 → panel recreatedrestore draft for tab 12
The panel document is recreated on return — its state comes back from storage, not from memory.

Choosing between global and per-tab

The choice is not technical; it is about what the panel represents. If the panel is a tool the user carries between pages — a notebook, a chat, a to-do list — it should be global, because switching tabs and finding the tool gone breaks the user’s flow. If the panel is about the page — its summary, its accessibility report, its pull request — it should be per-tab, because showing GitHub review comments next to a news article is nonsense.

Some extensions need both: a global tool with a page-aware section. That is usually better built as a global panel that listens for active-tab changes and updates one region, rather than a per-tab panel that re-creates the whole tool on every switch.

1// Global panel with a page-aware section
2chrome.tabs.onActivated.addListener(async ({ tabId }) => {
3  const tab = await chrome.tabs.get(tabId);
4  pageSection.replaceChildren(await pageInfo(tab));   // only this part changes
5});

Execution context: the side panel document of a global panel, which can register its own chrome.tabs listeners because it is an extension page with the extension’s permissions. The rest of the panel — the user’s notes, their scroll position — stays exactly as it was.

Global, per-tab, or global with a page-aware section?A decision tree based on whether the panel is a tool the user carries between pages or information about the current page.What is the panel mostly about?A tool the user carriesGlobal panelone documentNever disappears on tab switchnotes, chat, listsThe current pagePer-tab panelsetOptions({tabId})Disabled where it does not applystate per tab in session storageBothGlobal + page sectiontabs.onActivated in the panelOnly one region refreshestool state untouched
The hybrid is often the best experience — the tool stays, only the page-specific part refreshes.

Cross-browser variation

  • Chrome / Edge: sidePanel.setOptions({ tabId, path, enabled }) from Chrome 114. Tab-scoped panels close when switching to a tab where the panel is disabled or has a different path, and reopen on return.
  • Firefox: sidebarAction.setPanel({ tabId, panel }) provides per-tab panel URLs for the sidebar, and sidebarAction per-window options exist too. There is no per-tab enable/disable; the sidebar is either open or not in the window.
  • Safari: no side panel. Per-page information belongs in the popup or an injected panel.
  • All three: tab ids are reused after tabs close. Clean up per-tab storage keys on tabs.onRemoved so a new tab never inherits an old tab’s panel state.

Verification

  1. Open the panel on a supported tab, switch to an unsupported one and back; confirm it hides and returns with its draft intact.
  2. Inspect the options for a tab:
1await chrome.sidePanel.getOptions({ tabId });
2// { enabled: true, path: "panel.html?tab=12" }

Execution context: the service worker console. On an unsupported tab this should report enabled: false.

  1. Navigate a supported tab to an unsupported site and confirm the panel disappears without switching tabs.
  2. Close a tab and confirm its panel:<id> key is removed from session storage.

FAQ

Does a per-tab panel keep running when its tab is in the background?

No — the panel document is torn down when the user switches to a tab with different options. Persist anything that matters, and do not rely on timers inside a per-tab panel.

Can two tabs show the same panel document at once?

The panel is per window, so only the active tab’s panel is visible. Two windows can each show a panel for their own active tab.

Is the tabs permission required?

Only to read tab.url in the background to decide where to enable the panel. If the decision can be made on a user gesture with activeTab, you can avoid it.

Other UI/UX Patterns & Interactive Components Resources