Chrome Extension Side Panel — Manifest V3 Guide

5 min read

Side Panel API in Chrome Extensions

Introduction

manifest.json

{
  "permissions": ["sidePanel"],
  "side_panel": {
    "default_path": "sidepanel.html"
  }
}

Basic Side Panel

<!-- sidepanel.html -->
<!DOCTYPE html>
<html>
<head><style>body { width: 300px; font-family: sans-serif; }</style></head>
<body>
  <h1>My Side Panel</h1>
  <div id="content"></div>
  <script src="sidepanel.js"></script>
</body>
</html>

Opening the Side Panel

From User Action (Toolbar Click)

// background.js — open side panel when extension icon is clicked
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });

Programmatically

// From background service worker
chrome.sidePanel.open({ windowId: chrome.windows.WINDOW_ID_CURRENT });

// For a specific tab
chrome.sidePanel.open({ tabId: tab.id });

Per-Tab Side Panels

// Set different panel content for specific tabs
chrome.sidePanel.setOptions({
  tabId: tab.id,
  path: "tab-specific-panel.html",
  enabled: true
});

chrome.sidePanel API

setOptions(options)

chrome.sidePanel.setOptions({
  path: "sidepanel.html",      // Panel HTML file
  enabled: true,                // Enable/disable
  tabId: tab.id                 // Optional: tab-specific
});

getOptions(options)

const options = await chrome.sidePanel.getOptions({ tabId: tab.id });
console.log(options.path, options.enabled);

setPanelBehavior(behavior)

chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });

getPanelBehavior()

const behavior = await chrome.sidePanel.getPanelBehavior();
console.log(behavior.openPanelOnActionClick);

open(options) (Chrome 116+)

await chrome.sidePanel.open({ windowId: windowId });

Side Panel vs Popup

| Feature | Side Panel | Popup | |———|———–|——-| | Persistence | Stays open while browsing | Closes on click outside | | Size | Full browser height, ~300-400px wide | Fixed small window | | Tab awareness | Can change per-tab | Same for all tabs | | User interaction | Doesn’t interrupt browsing | Requires focus | | Use case | Reference content, tools, chat | Quick actions, settings |

Communication with Background

// sidepanel.js — using @theluckystrike/webext-messaging
const messenger = createMessenger<Messages>();

// Get data from background
const data = await messenger.sendMessage('getData', { key: 'value' });

// Listen for updates from background
messenger.onMessage('updatePanel', (data) => {
  document.getElementById('content').textContent = JSON.stringify(data);
});

Reactive Updates with Storage

// sidepanel.js — react to storage changes in real-time
import { createStorage, defineSchema } from '@theluckystrike/webext-storage';
const storage = createStorage(defineSchema({ notes: 'string', theme: 'string' }), 'local');

// Watch for changes (e.g., from background or options page)
storage.watch('notes', (newValue) => {
  document.getElementById('notes').textContent = newValue;
});

// Save user input
document.getElementById('input').addEventListener('input', async (e) => {
  await storage.set('notes', e.target.value);
});

Common Patterns

Reading Assistant

Note-Taking Tool

Chat/AI Assistant

Developer Tools

Best Practices

Common Mistakes

Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.