Implementation of Side Panel for Browser Extension (Chrome)
Side Panel is a Chrome sidebar that appeared in Chrome 114. It works as a persistent panel beside page content, doesn't close when switching tabs, and has significantly more space than Popup. This is the first native mechanism for "pinned" interfaces in Chrome.
Side Panel vs Popup differences
| Characteristic | Popup | Side Panel |
|---|---|---|
| Width | 800 px max | ~400 px (fixed by Chrome) |
| Lifetime | Until focus loss | Persistent, survives tab switches |
| Tab context | Bound to active | Can be global or per-tab |
| Availability | Since Chrome 4 | Since Chrome 114 |
| Firefox/Safari | No analog | No analog |
Connection in Manifest V3
{
"manifest_version": 3,
"name": "My Side Panel Extension",
"version": "1.0.0",
"permissions": ["storage", "tabs", "activeTab", "scripting", "sidePanel"],
"background": {
"service_worker": "background.js"
},
"action": {
"default_icon": "icons/icon48.png",
"default_title": "Open panel"
},
"side_panel": {
"default_path": "panel/panel.html"
}
}
Opening panel on icon click
By default, clicking the extension icon doesn't do anything with Side Panel. Must explicitly configure behavior:
// background.js (Service Worker)
// Option 1: open panel on every icon click
chrome.sidePanel
.setPanelBehavior({ openPanelOnActionClick: true })
.catch(console.error);
// Option 2: open programmatically (requires user gesture)
chrome.action.onClicked.addListener(async (tab) => {
await chrome.sidePanel.open({ tabId: tab.id });
});
Per-tab vs global panel
Panel can be different for each tab or shared across all:
// background.js
// Different panel depending on site
chrome.tabs.onUpdated.addListener(async (tabId, info, tab) => {
if (info.status !== 'complete') return;
if (tab.url?.includes('github.com')) {
await chrome.sidePanel.setOptions({
tabId,
path: 'panel/github-panel.html',
enabled: true,
});
} else if (tab.url?.includes('figma.com')) {
await chrome.sidePanel.setOptions({
tabId,
path: 'panel/design-panel.html',
enabled: true,
});
} else {
// Disable panel for regular pages
await chrome.sidePanel.setOptions({ tabId, enabled: false });
}
});
React app in Side Panel
Structure of panel/panel.html is similar to Popup, but has more space:
// panel/App.tsx
import { useEffect, useState, useRef } from 'react';
import browser from 'webextension-polyfill';
export function SidePanel() {
const [notes, setNotes] = useState<string[]>([]);
const [currentUrl, setCurrentUrl] = useState('');
const portRef = useRef<browser.Runtime.Port | null>(null);
useEffect(() => {
// Persistent connection with background for data streaming
portRef.current = browser.runtime.connect({ name: 'side-panel' });
portRef.current.onMessage.addListener((msg) => {
if (msg.type === 'PAGE_CHANGED') {
setCurrentUrl(msg.url);
loadNotesForUrl(msg.url);
}
});
return () => portRef.current?.disconnect();
}, []);
return (
<div className="side-panel">
<header className="side-panel__header">
<h2>Notes</h2>
</header>
{/* Panel content */}
</div>
);
}
Timeline
Basic side panel with notes: 2–3 days. Complex with sync and per-tab logic: 5–7 days.







