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.

Published October 2, 2026 Updated October 2, 2026 7 min read
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.

First-run state drives the popuponInstalled sets onboarding to new; the popup reads it on open and shows the first-run view with one action; completing the action sets it to done and the popup switches to the regular view on the next open.onInstalled (install)onboarding = newPopup opensread stateFirst-run viewone actionfirst successonboarding = donestorage.localRegular viewlists, tabs, searchHelp linkre-open tips
One stored flag, two popup views, one transition.

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.

What the first-run popup showsDecision tree: on a supported page, show the one first action; on an unsupported page, explain where the extension works with an example; if site access was withheld, explain and offer to grant it.What is the current tab?supported pagePrimary action"Save this page"Success statethen regular viewbrowser / blank pageWhere it works"Open any article…"access withheldExplain + grantsite access button
Adapt the first-run view to the tab the user is on.

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.

First run to first successThe user clicks the icon on an article for the first time; the popup shows the welcome view with Save this page focused; the user presses it; the worker saves; the popup shows a success message, records onboarding done, then switches to the regular list with the saved item on top.UserPopupService workerfirst icon clickwelcome + [Save this page]Entersave-pageok"Saved!" → regular view
Under ten seconds from first click to first value.

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.getUserSettings reports pinning; popups open with activeTab.
  • 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

  1. Install fresh, open the popup on an article and confirm the first-run view with focus on the primary action.
  2. Complete the action and confirm the transition and that the next open is the regular view.
  3. Update the extension and confirm onboarding does not reappear.
  4. 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.

Other UI/UX Patterns & Interactive Components Resources