Side Panel development patterns for Chrome Extensions covering manifest configuration, chrome.sidePanel API, programmatic control, per-tab vs global panels, lifecycle management, and communication patterns. Use when building side panel features.
Build Chrome Extension side panels with manifest config, chrome.sidePanel API, per-tab vs global panels, lifecycle management, and background/content script communication.
/plugin marketplace add francanete/fran-marketplace/plugin install chrome-extension-expert@fran-marketplaceThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Side panels provide a persistent UI alongside web pages, offering richer interaction than popups.
Key Characteristics:
sidePanel permission{
"manifest_version": 3,
"name": "Side Panel Extension",
"version": "1.0.0",
"permissions": ["sidePanel"],
"side_panel": {
"default_path": "sidepanel.html"
},
"action": {
"default_title": "Open Side Panel"
},
"background": {
"service_worker": "background.js"
}
}
<!-- sidepanel.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="sidepanel.css">
</head>
<body>
<div id="app">
<header>
<h1>Side Panel</h1>
</header>
<main id="content">
<!-- Content here -->
</main>
</div>
<script src="sidepanel.js"></script>
</body>
</html>
// background.js
// Open on action click
chrome.sidePanel.setPanelBehavior({
openPanelOnActionClick: true
});
// Or manually open on click
chrome.action.onClicked.addListener(async (tab) => {
await chrome.sidePanel.open({ tabId: tab.id });
});
// Open for entire window
chrome.action.onClicked.addListener(async (tab) => {
await chrome.sidePanel.open({ windowId: tab.windowId });
});
// Set panel for specific tab
await chrome.sidePanel.setOptions({
tabId: tab.id,
path: 'sidepanel.html',
enabled: true
});
// Disable panel for specific tab
await chrome.sidePanel.setOptions({
tabId: tab.id,
enabled: false
});
// Set default panel (all tabs)
await chrome.sidePanel.setOptions({
path: 'sidepanel.html',
enabled: true
});
// Get options for specific tab
const options = await chrome.sidePanel.getOptions({ tabId: tab.id });
console.log('Panel path:', options.path);
console.log('Enabled:', options.enabled);
// Get default options
const defaultOptions = await chrome.sidePanel.getOptions({});
// Auto-open on extension icon click
await chrome.sidePanel.setPanelBehavior({
openPanelOnActionClick: true
});
// Get current behavior
const behavior = await chrome.sidePanel.getPanelBehavior();
console.log('Opens on click:', behavior.openPanelOnActionClick);
// background.js - Set once
chrome.runtime.onInstalled.addListener(() => {
chrome.sidePanel.setOptions({
path: 'sidepanel.html',
enabled: true
});
});
// background.js
chrome.tabs.onActivated.addListener(async ({ tabId }) => {
const tab = await chrome.tabs.get(tabId);
// Different panel based on URL
if (tab.url?.includes('github.com')) {
await chrome.sidePanel.setOptions({
tabId,
path: 'sidepanel-github.html'
});
} else if (tab.url?.includes('docs.google.com')) {
await chrome.sidePanel.setOptions({
tabId,
path: 'sidepanel-docs.html'
});
} else {
await chrome.sidePanel.setOptions({
tabId,
path: 'sidepanel-default.html'
});
}
});
// Also handle URL changes within tab
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
if (changeInfo.url) {
// Update panel based on new URL
await updatePanelForTab(tabId, changeInfo.url);
}
});
// sidepanel.js
document.addEventListener('DOMContentLoaded', async () => {
console.log('Side panel loaded');
// Get current tab
const [tab] = await chrome.tabs.query({
active: true,
currentWindow: true
});
// Initialize with tab data
await initializePanel(tab);
});
// Clean up before unload
window.addEventListener('beforeunload', () => {
// Save state
saveCurrentState();
});
// Visibility change (panel minimized/restored)
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
console.log('Panel hidden');
} else {
console.log('Panel visible');
}
});
// sidepanel.js
// Save state when it changes
async function saveState(state) {
await chrome.storage.session.set({ sidePanelState: state });
}
// Restore state on load
async function restoreState() {
const { sidePanelState } = await chrome.storage.session.get('sidePanelState');
if (sidePanelState) {
applyState(sidePanelState);
}
}
// Tab-specific state
async function saveTabState(tabId, state) {
const key = `sidePanelState_${tabId}`;
await chrome.storage.session.set({ [key]: state });
}
async function getTabState(tabId) {
const key = `sidePanelState_${tabId}`;
const result = await chrome.storage.session.get(key);
return result[key];
}
// sidepanel.js
async function fetchData() {
const response = await chrome.runtime.sendMessage({
type: 'FETCH_DATA',
query: 'search term'
});
if (response.success) {
displayData(response.data);
}
}
// background.js
function notifySidePanel(data) {
chrome.runtime.sendMessage({
type: 'DATA_UPDATE',
data
}).catch(() => {
// Side panel might not be open
});
}
// sidepanel.js
async function getPageData() {
const [tab] = await chrome.tabs.query({
active: true,
currentWindow: true
});
// Request goes through background
const response = await chrome.runtime.sendMessage({
type: 'GET_PAGE_DATA',
tabId: tab.id
});
return response;
}
// background.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'GET_PAGE_DATA') {
chrome.tabs.sendMessage(message.tabId, { type: 'EXTRACT_DATA' })
.then(sendResponse)
.catch(error => sendResponse({ error: error.message }));
return true;
}
});
// sidepanel.js
let port;
function connectToBackground() {
port = chrome.runtime.connect({ name: 'sidepanel' });
port.onMessage.addListener((message) => {
handleBackgroundMessage(message);
});
port.onDisconnect.addListener(() => {
console.log('Disconnected, reconnecting...');
setTimeout(connectToBackground, 1000);
});
}
document.addEventListener('DOMContentLoaded', connectToBackground);
function sendToBackground(message) {
if (port) {
port.postMessage(message);
}
}
| Feature | Side Panel | Popup |
|---|---|---|
| Persistence | Stays open while browsing | Closes on outside click |
| Size | Full height, ~400px width | Max 800x600 |
| Use Case | Ongoing tasks, reference | Quick actions |
| Tab Context | Can track active tab | Single tab context |
| Performance | Always loaded when open | Loaded on demand |
| Minimum Chrome | 114+ | All versions |
Choose Side Panel when:
Choose Popup when:
/* sidepanel.css */
:root {
--panel-padding: 16px;
}
body {
margin: 0;
padding: var(--panel-padding);
font-family: system-ui, sans-serif;
font-size: 14px;
/* Panel width is typically 320-400px */
min-width: 280px;
max-width: 100%;
}
/* Stack layout for narrow panels */
.container {
display: flex;
flex-direction: column;
gap: 12px;
}
// sidepanel.js
async function updateHeader() {
const [tab] = await chrome.tabs.query({
active: true,
currentWindow: true
});
document.getElementById('site-name').textContent =
new URL(tab.url).hostname;
document.getElementById('favicon').src = tab.favIconUrl || '';
}
// Update when tab changes
chrome.tabs.onActivated.addListener(updateHeader);
chrome.tabs.onUpdated.addListener((tabId, changeInfo) => {
if (changeInfo.title || changeInfo.favIconUrl) {
updateHeader();
}
});
// sidepanel.js
function showLoading() {
document.getElementById('content').innerHTML = `
<div class="loading">
<div class="spinner"></div>
<p>Loading...</p>
</div>
`;
}
function showError(message) {
document.getElementById('content').innerHTML = `
<div class="error">
<p>${message}</p>
<button onclick="retry()">Retry</button>
</div>
`;
}
/* sidepanel.css */
#app {
display: flex;
flex-direction: column;
height: 100vh;
}
header {
flex-shrink: 0;
padding: 12px;
border-bottom: 1px solid #e0e0e0;
}
main {
flex: 1;
overflow-y: auto;
padding: 12px;
}
footer {
flex-shrink: 0;
padding: 12px;
border-top: 1px solid #e0e0e0;
}
// background.js
const tabPanelStates = new Map();
chrome.tabs.onActivated.addListener(async ({ tabId, windowId }) => {
// Get or create state for this tab
let state = tabPanelStates.get(tabId);
if (!state) {
state = { initialized: false, data: null };
tabPanelStates.set(tabId, state);
}
// Notify side panel of tab change
chrome.runtime.sendMessage({
type: 'TAB_CHANGED',
tabId,
state
}).catch(() => {});
});
// Clean up when tab closes
chrome.tabs.onRemoved.addListener((tabId) => {
tabPanelStates.delete(tabId);
});
// background.js
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
if (changeInfo.status === 'complete' && tab.url) {
// Only enable for supported sites
const supportedSites = [
'github.com',
'stackoverflow.com',
'developer.mozilla.org'
];
const isSupported = supportedSites.some(site =>
tab.url.includes(site)
);
await chrome.sidePanel.setOptions({
tabId,
enabled: isSupported
});
}
});
// content.js
function setupMutationObserver() {
const observer = new MutationObserver((mutations) => {
// Detect relevant page changes
const relevantChanges = filterRelevantMutations(mutations);
if (relevantChanges.length > 0) {
chrome.runtime.sendMessage({
type: 'PAGE_CONTENT_CHANGED',
changes: relevantChanges
});
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
// sidepanel.js - Listen for updates
chrome.runtime.onMessage.addListener((message) => {
if (message.type === 'PAGE_CONTENT_CHANGED') {
refreshData(message.changes);
}
});
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.