C.W.K.
Stream
Lesson 01 of 06 · published

What the Side Panel Is — Chrome's Persistent Dock

~10 min · side-panel, concepts, ux, chrome-114

Level 0Extension Curious
0 XP0/54 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"The popup is a tooltip. The side panel is a dock. Track 4 is about graduating ClipDeck to a surface the user can leave open while they keep reading the page — the difference between a sticky note and a notebook on the desk."

The Surface, Plainly

A side panel is an extension-owned HTML document that Chrome renders in a docked region to the right of the user's tab. Visually it sits between the bookmarks bar and the toolbar's right edge. Functionally it is an extension page just like the popup — same chrome.* API access, same isolated context, same DevTools-inspectable.

The key difference is lifetime: the popup dies the instant the user clicks anywhere else; the side panel stays open until the user explicitly closes it via the toolbar toggle. For ClipDeck, that means the user can keep the clip list visible while they read an article and add new clips one after another, without re-opening anything between saves.

The History

chrome.sidePanel landed in stable Chrome 114 around mid-2023. Before that, the only way to mimic a docked sidebar was to inject an iframe via content script — fragile on sites with strict CSP, broken on cross-origin embeds, doomed on view-source pages. The stable API gives you a Chrome-owned region that does not collide with the page's DOM at all.

Anything built today should assume Chrome 114 as the minimum. Firefox has its own sidebar API with a different shape; this lesson is Chrome-specific. Track 8's packaging notes touch on cross-browser strategy at the end.

The Three User-Facing Behaviors

  • Opens on toolbar-icon click (configurable via chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true })). Easiest to discover.
  • Opens via API call from any user-gesture handler — context-menu entry, keyboard command, or a button in the popup that triggers chrome.sidePanel.open(). Lets you bind a custom shortcut.
  • Stays open across tab switches by default. The displayed panel content can change per tab (next lesson covers per-tab vs global), but Chrome does not auto-close the dock when the user switches tabs.

What Lives Inside

A side panel is just panel.html plus its scripts and styles. You can render whatever you want — vanilla DOM, a React/Svelte/Solid app, an iframe pointing at a self-hosted URL. The HTML is loaded under the extension's content security policy, so external scripts and inline event handlers are blocked unless you broaden the CSP (Lesson 5 covers this).

Storage access works the same way it does everywhere: chrome.storage.local on mount, chrome.storage.onChanged for live updates. Message passing to the SW is the same chrome.runtime.sendMessage you've been using.

Why ClipDeck Needs One

The popup served the first two tracks well — it's small, fast, and obvious. Past 10 clips, the popup feels cramped. Past 50 clips, it's unusable. The side panel scales: full-height list with timestamps, source URLs, search, and (Track 7) inline edit/delete affordances. The popup keeps existing as a quick-status surface ("how many clips today?") and as the place that hosts the "Open side panel" button.

Popup = tooltip (transient, small, opens on click). Side panel = dock (persistent, full-height, stays until dismissed). They cover different UX needs; ClipDeck ships both.
Chrome side panel vs Firefox sidebar. Chrome's chrome.sidePanel and Firefox's browser.sidebarAction look similar but have different lifecycle models and slightly different APIs. Cross-browser extensions usually feature-detect and ship two implementations. This quest stays in Chrome; ClipDeck v2 might add Firefox parity, but that's a separate concern.

Code

manifest.json — version 0.6.0 with side_panel.default_path declared·json
{
  "manifest_version": 3,
  "name": "ClipDeck",
  "version": "0.6.0",
  "action": { "default_popup": "popup.html" },
  "background": { "service_worker": "background.js" },
  "side_panel": { "default_path": "panel.html" },
  "permissions": ["storage", "tabs", "scripting", "activeTab", "sidePanel"],
  "content_scripts": [
    { "matches": ["<all_urls>"], "js": ["content.js"], "run_at": "document_idle" }
  ],
  "icons": { "16": "icons/16.png", "48": "icons/48.png", "128": "icons/128.png" }
}
panel.html — minimum-viable side panel markup·html
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>ClipDeck</title>
    <style>
      body { font-family: system-ui, sans-serif; margin: 0; padding: 16px; }
      h1 { font-size: 18px; margin: 0 0 12px; }
      .empty { color: #888; font-style: italic; }
    </style>
  </head>
  <body>
    <h1>ClipDeck — Saved Clips</h1>
    <div id="list" class="empty">No clips yet. Press Ctrl+Shift+K on selected text.</div>
    <script src="panel.js"></script>
  </body>
</html>
panel.js — mount, render, subscribe·javascript
// panel.js — read clips, render, subscribe to changes
async function render() {
  const { clips = [] } = await chrome.storage.local.get("clips");
  const list = document.getElementById("list");
  list.className = clips.length === 0 ? "empty" : "";
  list.innerHTML = clips.length === 0
    ? "No clips yet. Press Ctrl+Shift+K on selected text."
    : clips
        .map((c) => `<div><strong>${escapeHtml(c.title || c.url)}</strong><br><small>${escapeHtml(c.text.slice(0, 200))}</small></div>`)
        .join("<hr/>");
}

function escapeHtml(s) {
  return String(s).replace(/[&<>"']/g, (ch) => ({
    "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;",
  })[ch]);
}

chrome.storage.onChanged.addListener((changes, area) => {
  if (area === "local" && "clips" in changes) render();
});

render();

External links

Exercise

Add the manifest changes from the first code block (version 0.6.0, side_panel.default_path: "panel.html", "sidePanel" in permissions). Create clipdeck/panel.html and clipdeck/panel.js with the second and third code blocks. Reload the extension. Click the ClipDeck toolbar icon — the popup should still appear, NOT the panel (we have not yet enabled openPanelOnActionClick). Now click the side-panel toggle in Chrome's toolbar (the puzzle-piece menu or the dedicated side panel icon, depending on Chrome version) and pick ClipDeck. The panel opens. If you have saved clips from Track 3's exercise, they appear; if not, save a few via Ctrl+Shift+K and confirm the panel updates without manual refresh.
Hint
If the side-panel toggle does not show ClipDeck, check that "sidePanel" is in permissions AND side_panel.default_path is set, then fully reload the extension (chrome://extensions → ClipDeck → reload). If the panel opens but shows a blank page, open the panel's DevTools (right-click anywhere in the panel → Inspect) and look for JS errors — the most common is a typo in the <script src="panel.js"> path. The panel is a regular extension page; you can use the same DevTools workflows you use for the popup.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.