Skip to content

Sidepanel Extension

This Extension demonstrates how to use the Side Panel API to create a persistent side panel that can be opened alongside web pages. The extension includes controls to toggle the side panel position (left/right) and configure whether clicking the extension icon opens the sidepanel instead of the popup.

  • Directorysidepanel-example
    • manifest.json
    • background.js
    • popup.html
    • popup.js
    • sidepanel.html
    • sidepanel.js
    • styles.css
{
"manifest_version": 3,
"name": "Sidepanel Example",
"version": "1.0",
"action": {
"default_popup": "popup.html"
},
"side_panel": {
"default_path": "sidepanel.html"
},
"background": {
"service_worker": "background.js"
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sidepanel Example</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<div class="card">
<h1>Sidepanel Example</h1>
<p class="subtitle">Open the sidepanel to access more options</p>
<button id="openSidepanel" class="btn btn-primary">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<line x1="15" y1="3" x2="15" y2="21"></line>
</svg>
Open Sidepanel
</button>
</div>
</div>
<script src="popup.js"></script>
</body>
</html>
document.getElementById('openSidepanel').addEventListener('click', async () => {
browser.action.closePopup();
browser.sidePanel.open();
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sidepanel</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<div class="card">
<div class="status-badge">
<span class="dot"></span>
Sidepanel Active
</div>
<h1>Sidepanel Controls</h1>
<p class="subtitle">Manage your sidepanel preferences</p>
<div class="toggle-group">
<div class="toggle-item">
<div class="toggle-label">
<span class="toggle-title">Sidepanel Position</span>
<span class="toggle-description">Toggle between left and right side</span>
<div id="sideIndicator" class="side-indicator right">
<span class="dot"></span>
<span>Left</span>
<span>|</span>
<span>Right</span>
<span class="dot"></span>
</div>
</div>
<label class="toggle-switch">
<input type="checkbox" id="sideToggle" checked>
<span class="toggle-slider"></span>
</label>
</div>
<div class="toggle-item">
<div class="toggle-label">
<span class="toggle-title">Open on Icon Click</span>
<span class="toggle-description">Clicking extension icon opens sidepanel instead of popup</span>
</div>
<label class="toggle-switch">
<input type="checkbox" id="behaviorToggle">
<span class="toggle-slider"></span>
</label>
</div>
</div>
</div>
</div>
<script src="sidepanel.js"></script>
</body>
</html>
// DOM elements
const sideToggle = document.getElementById('sideToggle');
const behaviorToggle = document.getElementById('behaviorToggle');
const sideIndicator = document.getElementById('sideIndicator');
// Side toggle (right = checked, left = unchecked)
sideToggle.addEventListener('change', () => {
const side = sideToggle.checked ? 'right' : 'left';
updateSideIndicator(side);
browser.sidePanel.setLayout({ side });
});
// Behavior toggle
behaviorToggle.addEventListener('change', () => {
const openOnClick = behaviorToggle.checked;
browser.sidePanel.setPanelBehavior({ openPanelOnActionClick: openOnClick });
});
// Update side indicator visual
function updateSideIndicator(side) {
sideIndicator.className = `side-indicator ${side}`;
}
browser.sidePanel.onOpened.addListener(async () => {
const { text } = await browser.action.getPopupBadgeText();
const count = +(text || 0);
browser.action.setPopupBadgeText(String(count + 1));
});
browser.sidePanel.onClosed.addListener(async () => {
const { text } = await browser.action.getPopupBadgeText();
const count = +(text || 0);
browser.action.setPopupBadgeText(String(count + 1));
});
View styles.css
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
color: #1f2937;
}
.container {
padding: 8px 16px;
min-width: 300px;
}
.card {
background: white;
border-radius: 16px;
padding: 24px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
}
h1 {
font-size: 20px;
font-weight: 600;
color: #1f2937;
margin-bottom: 8px;
text-align: center;
}
.subtitle {
font-size: 13px;
color: #6b7280;
text-align: center;
margin-bottom: 24px;
}
.btn {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
padding: 14px 20px;
border: none;
border-radius: 12px;
font-size: 15px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-primary {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
color: white;
box-shadow: 0 4px 14px rgba(99, 102, 241, 0.4);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(99, 102, 241, 0.5);
}
.btn-primary:active {
transform: translateY(0);
}
.btn svg {
width: 18px;
height: 18px;
}
/* Toggle Switch */
.toggle-group {
margin-top: 20px;
}
.toggle-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
background: #f9fafb;
border-radius: 12px;
margin-bottom: 12px;
}
.toggle-item:last-child {
margin-bottom: 0;
}
.toggle-label {
display: flex;
flex-direction: column;
gap: 4px;
}
.toggle-title {
font-size: 14px;
font-weight: 500;
color: #1f2937;
}
.toggle-description {
font-size: 12px;
color: #6b7280;
}
.toggle-switch {
position: relative;
width: 52px;
height: 28px;
flex-shrink: 0;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #d1d5db;
border-radius: 28px;
transition: all 0.3s ease;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 22px;
width: 22px;
left: 3px;
bottom: 3px;
background: white;
border-radius: 50%;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
input:checked + .toggle-slider {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
}
input:checked + .toggle-slider:before {
transform: translateX(24px);
}
input:focus + .toggle-slider {
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.3);
}
/* Side indicator */
.side-indicator {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
color: #9ca3af;
margin-top: 2px;
}
.side-indicator .dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #d1d5db;
transition: background 0.3s ease;
}
.side-indicator.left .dot:first-child,
.side-indicator.right .dot:last-child {
background: #6366f1;
}
/* Status badge */
.status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: #f0fdf4;
border-radius: 20px;
font-size: 12px;
color: #15803d;
margin-bottom: 20px;
}
.status-badge .dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #22c55e;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* Divider */
.divider {
height: 1px;
background: #e5e7eb;
margin: 20px 0;
}
  • Side Panel Configuration: The side_panel property in manifest.json defines the default side panel HTML file:
"side_panel": {
"default_path": "sidepanel.html"
}
  • Opening the Side Panel: From the popup, you can programmatically open the side panel:
browser.action.closePopup();
browser.sidePanel.open();
  • Setting Side Panel Position: Use browser.sidePanel.setLayout() to control whether the side panel appears on the left or right:
browser.sidePanel.setLayout({ side: 'right' }); // or 'left'
  • Configuring Icon Click Behavior: Control whether clicking the extension icon opens the sidepanel instead of the popup:
browser.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });
  • Monitoring Side Panel Events: Listen for side panel open/close events in the background script:
browser.sidePanel.onOpened.addListener(() => {
// Handle side panel opened
});
browser.sidePanel.onClosed.addListener(() => {
// Handle side panel closed
});