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.
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
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.
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.
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, andsidebarActionper-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.onRemovedso a new tab never inherits an old tab’s panel state.
Verification
- Open the panel on a supported tab, switch to an unsupported one and back; confirm it hides and returns with its draft intact.
- 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.
- Navigate a supported tab to an unsupported site and confirm the panel disappears without switching tabs.
- 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.
Related
- Opening the side panel from a user gesture — opening the per-tab panel.
- Building a side panel UI in MV3 — the panel document itself.
- Detecting tab URL changes — the navigation events this depends on.
- Side panel and DevTools interfaces — the parent guide.