"Declare in the manifest. Manipulate from the service worker. Open under a user gesture. Three sentences, three lessons in one — and three real ways the API can quietly refuse to work if you skip any of them."
Step 1 — Declare in the Manifest
Two fields together register a default panel:
side_panel.default_path— the HTML file inside your extension that Chrome will load when the panel opens. Required for the panel to exist at all."sidePanel"inpermissions— required to callchrome.sidePanel.*from the service worker. Without it, the API itself is undefined.
That alone is enough for the panel to show up in Chrome's side-panel chooser. Users can pick it manually; nothing else is wired yet.
Step 2 — Configure Behavior From the SW
The chrome.sidePanel API exposes three methods you'll actually use:
chrome.sidePanel.setOptions({ tabId?, path, enabled })— set the panel HTML and enabled state, optionally per tab. Pass atabIdto target one tab; omit it for global defaults.chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: boolean })— toggle whether clicking the toolbar icon opens the panel instead of the popup. Mutually exclusive with the popup on icon click: either click opens the panel or click opens the popup.chrome.sidePanel.open({ tabId? | windowId? })— programmatically open the panel. Must be called from a user-gesture handler (action click, keyboard command, context menu) or the call rejects.
All three live in the SW. The panel itself does not configure its own visibility — that authority sits with the SW, which has the cross-tab perspective.
The User-Gesture Rule
chrome.sidePanel.open() is the strict one. Calling it from a timer, an alarm, a webRequest event, or a content-script message that did not originate from a user gesture will reject with Side panel can only be opened by a user gesture. The legitimate triggers:
chrome.action.onClicked— toolbar icon click. Only fires when there is nodefault_popupin the manifest.chrome.commands.onCommand— keyboard shortcut declared in the manifestcommandsblock.chrome.contextMenus.onClicked— right-click menu item.chrome.runtime.onMessagewhen the message came from a popup or panel responding to a real user click — Chrome treats that as a gesture-derived event.
This rule is a security baseline: an extension cannot pop a panel in the user's face without their action. Honor it; don't try to fake gestures with timeouts.
The Two-Step Per-Tab Pattern
For ClipDeck, the eventual side-panel pattern is per-tab — each tab gets its own panel context so we can later filter clips by current site or even maintain per-tab annotations. The setup:
- Manifest declares the default
pathand thesidePanelpermission. - SW listens to
chrome.tabs.onActivated. On each event, callchrome.sidePanel.setOptions({ tabId, path: 'panel.html', enabled: true }). - SW separately calls
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true })once at install/startup to make clicking the toolbar icon open the panel instead of the popup — if that's the UX you want.
The Popup Coexistence Choice
ClipDeck ships both popup and panel. You have to pick which the toolbar icon opens by default:
- Keep
action.default_popup: "popup.html"ANDopenPanelOnActionClick: false→ toolbar click opens popup, panel only opens via its own toggle or a button. - Remove
default_popupAND setopenPanelOnActionClick: true→ toolbar click opens panel; popup is unreachable from the icon (you can still open it via keyboard or another flow). - Keep both: not possible. Chrome forces a choice on each click.
ClipDeck's choice for now: keep the popup as default (it loads faster, has the "reset / quick stats" buttons) and add a "Open clip list" button in the popup that calls chrome.sidePanel.open() in the user-gesture handler.
open() needs a user gesture. Miss any step, the panel either does not exist, does not appear, or rejects with a misleading error.setOptions({ tabId, path, enabled: true }) too early — before Chrome has finished registering the tab — it can silently fail. The safest pattern is to listen to chrome.tabs.onActivated and chrome.tabs.onUpdated together: re-apply setOptions whenever a tab becomes active OR finishes loading. Slightly redundant, immune to ordering.