Designing a First-Run Popup
Design what an extension's popup shows the first time it opens: detecting first run, one clear first action, permission and site-access explanations, sample content instead of empty states, pinning hints, and graduating to the regular popup.
Table of Contents
Someone installs the extension, clicks its icon for the first time, and sees the same popup a power user sees: an empty list, three tabs, a search box and a gear icon. They do not know what to do first, so they close it, and many never open it again. The first popup open is the moment of highest attention an extension ever gets, and it is wasted on a screen designed for returning users. A first-run popup replaces that screen with one clear first action, explains anything that needs explaining, and gets out of the way once the user has succeeded once. This guide designs it. It belongs to popup interface design.
First run is a state, not a page
The install tab (opened from onInstalled) is a common onboarding surface, but many users close it unread, and some installs — from enterprise policy or sync — never show it. The popup is the one surface every user who engages will see. Treat “first run” as a state stored in chrome.storage.local: absent or "new" until the user completes the core action once, then "done". The popup reads the state on open and renders the first-run view or the normal view. Completing the first action — saving a page, enabling on a site — moves the state on, so returning users never see onboarding again.
Step-by-step: a first-run popup
1. Record first-run state at install
1// sw.js
2chrome.runtime.onInstalled.addListener(async ({ reason }) => {
3 if (reason === chrome.runtime.OnInstalledReason.INSTALL) {
4 await chrome.storage.local.set({ onboarding: "new", installedAt: Date.now() });
5 }
6});
Execution context: the service worker. Only fresh installs get the state; updates must not re-trigger onboarding. storage.local is per-device, which is usually right — a user installing on a second computer may still appreciate a short first-run view, but if you prefer otherwise, check storage.sync for an existing account first. See handling every onInstalled reason.
2. Choose one first action
1first-run.md
2Extension: Readable (save and highlight articles)
3Job users installed it for: "save this article to read later"
4First action: [Save this page] — works on the current tab, instant result
5Not first: settings, sign-in, tags, import, keyboard shortcuts
Execution context: a design note. The first action should be the core job, doable right now on the current tab, with a visible result in seconds. Everything else — account, customisation, power features — can wait until the user has seen value. If the current page cannot support the action (a browser page, a blank tab), the first-run view explains where it works instead.
3. Render the first-run view
1// popup.js
2const { onboarding = "done" } = await chrome.storage.local.get("onboarding");
3const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
4const supported = /^https?:/.test(tab?.url ?? "");
5
6if (onboarding === "new") renderFirstRun({ supported });
7else renderRegular();
8
9function renderFirstRun({ supported }) {
10 const view = document.querySelector("#first-run");
11 view.hidden = false;
12 view.querySelector("h1").textContent = chrome.i18n.getMessage("welcomeTitle"); // "Save articles to read later"
13 const btn = view.querySelector("#first-action");
14 btn.textContent = chrome.i18n.getMessage(supported ? "saveThisPage" : "openAnArticle");
15 btn.disabled = !supported;
16 btn.focus();
17 btn.addEventListener("click", firstSave, { once: true });
18}
Execution context: the popup. The popup has the activeTab grant on open, so tab.url is readable for the current tab. The first-run view is a separate section of the same HTML, so there is no extra page load. Focusing the primary button lets keyboard users press Enter immediately, as in managing focus in popups and dialogs.
4. Celebrate the first success, then graduate
1async function firstSave() {
2 const result = await chrome.runtime.sendMessage({ type: "save-page" });
3 if (!result?.ok) return showError(result?.error);
4 await chrome.storage.local.set({ onboarding: "done" });
5 const view = document.querySelector("#first-run");
6 view.querySelector("#first-run-body").replaceChildren(
7 Object.assign(document.createElement("p"), { textContent: chrome.i18n.getMessage("firstSaveDone") }), // "Saved! Find it any time here."
8 );
9 setTimeout(renderRegular, 1500);
10}
Execution context: the popup. The success message shows where saved items live — the most useful next fact. Setting onboarding to "done" immediately means even if the popup closes now, the next open is the regular view. Transitioning to the regular view in place, with the newly saved item at the top of the list, teaches the interface by example.
5. Explain access only when it matters
If the extension needs host access the user has not granted — common in Firefox MV3, or when the user chose “On click” — the first-run view should say what will not work and offer a single Grant button that calls chrome.permissions.request from the click. Do not front-load every permission explanation; explain the one blocking the first action. See showing permission status on the options page.
6. Suggest pinning, gently
1const settings = await chrome.action.getUserSettings?.();
2if (settings && !settings.isOnToolbar) showHint(chrome.i18n.getMessage("pinHint")); // "Pin Readable to your toolbar for one-click access"
Execution context: the popup. action.getUserSettings() (Chrome 91+) reports whether the user pinned the extension. If they opened the popup from the extensions menu, a single hint explaining how to pin helps; never show it again after the first run.
7. Use sample content rather than an empty list
If the regular view would be empty after onboarding, show one example item marked as an example, or a short “How it works” list with three steps. An empty list with “No items” gives no direction. See popup loading and empty states.
8. Measure completion, privately
Count locally whether onboarding reached “done” and how long it took; if you collect usage metrics at all, send only aggregate, consented counts. A low completion rate means the first action is unclear or unavailable on the pages users visit. See privacy-preserving usage metrics.
Common mistakes
- The power-user popup on first open. No obvious first step.
- Onboarding that repeats after updates. Gate on the install reason.
- Several first actions. Pick one.
- Explaining every permission upfront. Explain only the blocking one.
- Requiring sign-in before any value. Show value first.
Cross-browser variation
- Chrome / Edge:
action.getUserSettingsreports pinning; popups open withactiveTab. - Firefox: host permissions are not granted at install in MV3, so the first-run view usually needs the grant step; there is no pinning API.
- Safari: users must enable the extension and grant website access in Safari settings first; the containing app usually handles that onboarding, and the popup can assume it is done.
Verification
- Install fresh, open the popup on an article and confirm the first-run view with focus on the primary action.
- Complete the action and confirm the transition and that the next open is the regular view.
- Update the extension and confirm onboarding does not reappear.
- Open on a browser page and confirm the explanation replaces the disabled action.
FAQ
Should first run also open an install tab?
Optional. A short welcome tab is fine, but the popup must stand on its own because many users skip the tab.
Can users see onboarding again?
Offer a “Show tips” link in help or settings that resets the state.
How long should the first-run popup text be?
A title and one sentence. The action explains the rest.
What if the user closes the popup without acting?
Show the first-run view again next time — the state is still new. After two or three dismissals, consider a quieter version with the regular view underneath, so the user is never stuck in onboarding.
Related
- Popup loading and empty states — after onboarding.
- Toasts and inline feedback in popups — the success message.
- Handling every onInstalled reason — install detection.
- Popup interface design — the parent topic.