"The popup has 200 milliseconds to feel useful before the user moves on. Lesson 2 is what that constraint actually means for size, focus, render order, and the half-dozen ways a sloppy popup loses trust."
The Size Window
Chrome auto-sizes popups to their content, within bounds:
Width: max ~800 px, but real-world sweet spot is 280–360 px. Wider than that and the popup feels like a misplaced tab.
Height: max ~600 px before Chrome scrolls. Try to stay under 480 px so the popup fits on a 13" laptop screen without scrolling.
Padding: 12–16 px is generous; 8 px is comfortable. Less than 8 and elements feel crammed.
Set explicit dimensions on body so the popup does not jitter as content loads:
Click-to-paint is the metric. From the moment the user clicks the toolbar icon to the moment the popup renders its first meaningful frame, you have ~200 ms before they notice latency. Three habits that buy that budget:
Render synchronously, refine async. Show the layout immediately with placeholder values ("0 clips" or "—"). Replace with real data as chrome.storage.local.get resolves. The user sees structure first, numbers second.
Avoid CSS-in-JS or big style frameworks. Vanilla CSS in a <style> tag (or a tiny external sheet) renders before the JS even runs. Frameworks that hydrate before paint can blow the budget.
Lazy-load expensive helpers. If you have a clipboard helper or a markdown renderer, import them inside the click handler that needs them, not at module top level.
Focus and Keyboard
Popups are keyboard-reachable: the user can hit Alt+Shift+T (or the configured shortcut), then Tab through. Three rules:
The first focusable element receives focus on open. If you want focus on the search input rather than the first button, add autofocus to the input. Test with Tab to confirm.
Escape should close the popup gracefully (Chrome already handles this, but if you intercept keydown, do not block Esc).
If your popup includes a list, make rows keyboard-navigable: each row gets tabindex="0" and an explicit Enter handler. Otherwise the user has to mouse it.
Single Purpose Per Popup
The popup is small. Resist the urge to put settings, library, and quick-action all in one tabbed view. The disciplined ClipDeck popup:
Top: two-line status ("5 clips today, 0 this hour").
Middle: 1–3 primary actions as buttons (Save current selection, Open clip list, Pause on this site).
Bottom: secondary links — "Settings," "Help," "View on GitHub." Plain text links, not buttons. Keeps the visual weight on the primary actions.
If the user needs to browse something, that lives in the side panel. If they need to configure something, that lives in the options page (Track 6). The popup is for one-shot intent.
State Updates While Open
If a popup stays open for a few seconds, subscribe to chrome.storage.onChanged just like the side panel does. The user can change something in another tab; the popup should reflect that immediately. Unsubscribe is automatic on popup close — the popup's JavaScript context dies, listeners die with it.
Popup = one shot, one purpose. Render structure first, refine with data. Sub-200 ms feels instant; sub-100 ms feels native. Anything browsing or configuring goes elsewhere.
The popup auto-close trap. Calling chrome.tabs.create, chrome.windows.create, or any chrome.action API that opens a window from inside the popup closes the popup immediately. Sometimes you want this (the action is complete, dismiss yourself); sometimes you don't (the user wanted to do two things). For two-step interactions, either defer the second step or move both steps to the side panel.
Code
popup.html — three primary actions, two secondary links·html
Replace clipdeck/popup.html with the first code block and clipdeck/popup.js with the second. Reload the extension. Click the toolbar icon — popup renders with placeholder zeros immediately, then the real counts pop in within milliseconds. Test the three primary buttons: Save current selection (select text first, then click) should add a clip; Open clip list opens the side panel and closes the popup; Pause on this site is wired in the next-to-last lesson, for now just confirm it's there. Open the popup, save a clip from another window via Ctrl+Shift+K, switch back — the popup count updates without re-opening.
Hint
If the placeholder zeros never replace with real numbers, refreshStats() is throwing — open the popup's DevTools (right-click in popup → Inspect → Console) and check for errors. A common one is the chrome:// special-page case where new URL(tab.url) works but the host filter returns no matches, leaving the siteCount at 0 with no obvious error. If the popup looks wider than expected, make sure box-sizing: border-box is set on body so the padding eats into the explicit width. If Save current selection alerts even on a real web page, your content script isn't loaded — confirm content.js is in content_scripts.matches.
Progress
Progress is local-only — sign in to sync across devices.