Side Panel & DevTools Interfaces
Implement persistent side panels and DevTools extension panels in MV3 using chrome.sidePanel and chrome.devtools.panels — per-tab control, lifecycle, and cross-browser gaps.
Implement persistent side panels and DevTools extension panels in MV3 using chrome.sidePanel and chrome.devtools.panels — per-tab control, lifecycle, and cross-browser gaps.
The single most important thing to understand before choosing a side panel over a popup is the availability constraint: chrome.sidePanel shipped in Chrome 114 (May 2023) and has no equivalent in Firefox or Safari. If your extension must run cross-browser, chrome.sidePanel is a Chrome-only progressive enhancement, not a baseline feature. That said, for Chrome-focused tools — developer utilities, reading companions, research aids — the side panel offers something the popup interface cannot: it stays open across page navigations. The popup renderer is destroyed the moment the user clicks elsewhere; the side panel persists in the browser chrome beside the page, surviving tab navigations and focus changes.
This is part of the broader UI/UX Patterns & Interactive Components surface for MV3 extensions. The second topic covered here — chrome.devtools.panels — is the older, more widely supported API for embedding custom panels inside the browser’s built-in DevTools window. DevTools panels have a different lifecycle again: they only exist while DevTools is open for a given tab, and they run in a dedicated devtools page context rather than the service worker or a side panel page.
Understanding which surface to choose is half the work. The diagram below maps the three contexts — popup, side panel, and DevTools panel — against what happens to them as the user navigates.
Before writing any side panel or DevTools panel code, confirm the following are in place:
"sidePanel" listed in the "permissions" array — without it every chrome.sidePanel.* call silently fails."side_panel": { "default_path": "sidepanel.html" } declared in manifest.json — this is the HTML file that loads inside the panel."devtools_page" key pointing to a dedicated HTML file (devtools.html). This file runs in its own isolated context and bootstraps chrome.devtools.panels.create()."action" key declared if you intend to use openPanelOnActionClick — the side panel behavior hooks into the toolbar button click. 1{
2 "manifest_version": 3,
3 "name": "My Panel Extension",
4 "version": "1.0",
5 "permissions": [
6 "sidePanel" // required for all chrome.sidePanel.* calls
7 ],
8 "action": {
9 "default_title": "Toggle side panel",
10 "default_icon": { "32": "icon32.png" }
11 },
12 "side_panel": {
13 "default_path": "sidepanel.html" // loaded when the panel opens
14 },
15 "devtools_page": "devtools.html", // bootstraps DevTools panel registration
16 "background": {
17 "service_worker": "background.js"
18 }
19}
Execution context: Parsed at install time by the extension host. side_panel.default_path sets the panel URL used for all tabs unless overridden per-tab with chrome.sidePanel.setOptions(). The devtools_page runs in a separate devtools context — one instance per open DevTools window — and is not the service worker.
By default clicking the toolbar action button does nothing with regard to the side panel. You must explicitly declare the intent with setPanelBehavior, which maps the action click to opening the panel, or call chrome.sidePanel.open() imperatively from an event.
1// background.js — service worker
2chrome.runtime.onInstalled.addListener(async () => {
3 // Make the toolbar button open the side panel on click.
4 // This replaces the default popup behaviour if action.default_popup is also set.
5 await chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });
6});
Execution context: Runs in the service worker. chrome.sidePanel.setPanelBehavior is a one-time persistent setting stored by the browser — you do not need to call it on every startup, but calling it inside onInstalled is the conventional guard. Chrome 114+ only. Firefox and Safari throw ReferenceError: chrome.sidePanel is undefined.
A common pattern is enabling the side panel only on relevant pages — for example, a reading tool that should only be active on article pages. chrome.sidePanel.setOptions() lets you toggle the panel on or off and even swap the panel URL for a specific tab.
1// background.js — service worker
2chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
3 if (changeInfo.status !== 'complete') return;
4
5 const isArticlePage = tab.url?.includes('example.com/articles/');
6
7 await chrome.sidePanel.setOptions({
8 tabId,
9 path: isArticlePage ? 'sidepanel.html' : undefined,
10 enabled: !!isArticlePage,
11 });
12});
Execution context: Runs in the service worker. tabId scopes the setting to one tab — other tabs are unaffected. When enabled: false the toolbar button is visually dimmed and clicking it does nothing. When enabled: true without a path, the side_panel.default_path from the manifest is used. The service worker may be terminated between tab events; registering the listener at the top level (not inside an async chain) ensures it is re-registered on wake.
chrome.sidePanel.open() opens the panel from code rather than waiting for the user to click the toolbar button. It must be called during a user gesture — typically inside a chrome.action.onClicked listener or a context menu click handler. Calling it outside a gesture throws a permission error.
1// background.js — service worker
2chrome.action.onClicked.addListener(async (tab) => {
3 // Open the panel for the current tab imperatively.
4 await chrome.sidePanel.open({ tabId: tab.id });
5});
Execution context: Runs in the service worker, triggered by a user gesture. The tabId argument is required — omitting it targets the current window’s active tab, but passing an explicit ID from the event is safer. This call and setPanelBehavior({ openPanelOnActionClick: true }) are mutually exclusive patterns: use one or the other per flow, not both, or the button click will fire twice.
Unlike the popup — which is destroyed the moment the user clicks away — the side panel persists across page navigations within the same tab. The panel page’s JavaScript runtime stays alive. This is useful for maintaining accumulated state (a reading list, an audit log, session data) without round-tripping through chrome.storage on every navigation.
The panel does close when the tab is closed or when the user manually closes it via the browser’s side panel dismiss button. Design for both cases.
1// sidepanel.js — runs in the side panel page
2let sessionLog = [];
3
4// Listen for messages from content scripts or the service worker
5chrome.runtime.onMessage.addListener((message, sender) => {
6 if (message.type === 'PAGE_DATA') {
7 sessionLog.push({ url: sender.tab?.url, data: message.payload });
8 renderLog(sessionLog); // update UI without re-fetching from storage
9 }
10});
11
12// Persist to storage only when panel is about to close
13window.addEventListener('beforeunload', async () => {
14 await chrome.storage.local.set({ sessionLog });
15});
Execution context: Runs in the side panel page (an isolated renderer process, similar to a popup but long-lived). chrome.runtime.onMessage listeners registered here survive page navigations in the main tab — the panel page itself does not navigate. For detailed UI implementation patterns, see building a side panel UI in MV3. For sharing data between the panel and the service worker, use chrome.storage or the message passing architecture.
chrome.devtools.panels.create() embeds a custom tab inside the browser’s built-in DevTools window. This API is available in Chrome, Firefox (via the WebExtensions DevTools API), and partially in Safari 17+. It predates chrome.sidePanel and has broader cross-browser coverage.
The devtools_page entry point is an invisible HTML page that runs per-DevTools-window. Its only job is to call chrome.devtools.panels.create(). The resulting panel runs in a separate panel page context.
1// devtools.js — loaded by devtools.html, runs in the devtools page context
2chrome.devtools.panels.create(
3 'My Extension', // tab title shown in DevTools
4 'icon16.png', // icon shown in the tab strip
5 'panel.html', // the HTML page rendered inside the panel
6 (panel) => {
7 // panel.onShown fires each time the user switches to this tab
8 panel.onShown.addListener((panelWindow) => {
9 panelWindow.init?.(); // call an init function in panel.html's context
10 });
11 panel.onHidden.addListener(() => {
12 // panel is in background — pause expensive operations
13 });
14 }
15);
Execution context: devtools.js runs in the devtools page context — a special sandboxed environment with access to chrome.devtools.* APIs but not chrome.tabs, chrome.storage, or most other extension APIs. To communicate with the service worker or content scripts, use chrome.runtime.sendMessage from the devtools page or from panel.html. The panel.html page itself runs in a standard extension page context with full access to extension APIs. Firefox maps this to browser.devtools.panels.create — use a polyfill for consistent naming.
chrome.sidePanel: There is no polyfill. Feature-detect with if (chrome.sidePanel) and fall back to a popup or notification for Firefox and Safari users.chrome.sidePanel.open(): Calling it outside a user-initiated event throws Error: User gesture required. Wire it to chrome.action.onClicked, a context menu item, or a keyboard command handler.chrome.sidePanel in content scripts: All side panel API calls must originate from the service worker or the side panel page itself — not from injected content scripts.chrome.tabs.onUpdated and chrome.action.onClicked listeners at the top level of background.js, not inside async chains or setTimeout callbacks.chrome.devtools.* APIs are only available in the devtools page context. chrome.storage, chrome.scripting, and most other APIs are blocked there; use message passing to reach the service worker.devtools_page lifecycle is tied to DevTools window: The devtools page is created when the user opens DevTools for a tab and destroyed when DevTools is closed. Do not store state there; persist it through chrome.storage or the service worker.eval in panel pages: MV3 CSP applies equally to side panel pages, DevTools panel pages, and the devtools bootstrap page. Build tools must emit external scripts.Chrome and Edge (Chromium) are the only browsers with chrome.sidePanel. Both share the same implementation from Chrome 114 / Edge 114 onwards. Per-tab setOptions and open() behave identically.
Firefox has no sidePanel API. Firefox’s MV3 implementation (available from Firefox 109+) includes chrome.devtools.panels mapped to browser.devtools.panels, so DevTools panels work there. If your extension depends on a persistent side-panel-style UI in Firefox, the only option is a sidebar_action (a legacy WebExtension API that Chrome does not support). You can conditionally register it in manifest.json using a Firefox-specific key — it is ignored by Chrome.
Safari added partial DevTools extension support in Safari 17. chrome.devtools.panels.create() works through the WebKit shim, but the panel UI rendering has known gaps with complex CSS. chrome.sidePanel is not implemented. Test Safari DevTools panels specifically because the panel.onShown / panel.onHidden timing differs from Chrome.
1// Cross-browser DevTools panel creation — works in Chrome, Firefox, Safari 17+
2const devtools = typeof browser !== 'undefined'
3 ? browser.devtools
4 : chrome.devtools;
5
6devtools.panels.create('My Panel', 'icon16.png', 'panel.html');
Execution context: Runs in the devtools page. The browser global is defined by Firefox’s WebExtension polyfill; Chrome exposes only chrome. Checking typeof browser avoids a ReferenceError on Chrome. Safari exposes browser via its shim since Safari 14.
| Feature | Chrome 114+ | Firefox 109+ | Safari 17+ |
|---|---|---|---|
chrome.sidePanel | Full | None | None |
chrome.devtools.panels | Full | Full (via browser.devtools) | Partial |
| Per-tab side panel control | Full | N/A | N/A |
sidebar_action (legacy) | None | Full | None |
DevTools panel.onShown timing | Accurate | Accurate | May lag one frame |
For pairing keyboard shortcuts with panel open/close, see Keyboard Shortcuts & Commands. If your extension mixes side panels with context menu triggers, the context menu patterns page covers how to call chrome.sidePanel.open() from a right-click handler safely.
chrome.sidePanel.open() for power-user access.chrome.sidePanel.* calls originate.