Skip to content
Get the kit

Extension pages

Your extension has pages of its own, next to the UI it injects into other sites. WXT turns every folder in apps/extension/entrypoints/ into a manifest entry. You never edit manifest.json.

PageOpensRendersModule
popuptoolbar icon clickaccount and settings tabs around your featurecore
sidepanelChrome’s side panel menuthe same tabs, docked, full heightsidepanel
welcomeonce, in a new tab, right after installthe three-step tourcore
newtabevery new tab (demo, off by default)a branded start pagedemo-newtab
devtools + devtools-panela panel inside DevTools (demo, off by default)the support log, livedemo-devtools

On-page UI (the status card, the highlighter) is a different kind of surface, covered in On-page UI.

The popup and the side panel are one screen

Section titled “The popup and the side panel are one screen”

Both pages mount the same component, AccountHub, with your feature as its child:

apps/extension/entrypoints/popup/main.tsx
<AppProviders>
<BroadcastBanner />
<AccountHub>
<YourFeature />
</AccountHub>
<PaywallOverlay />
</AppProviders>

AccountHub (components/account/AccountHub.tsx) is two tabs, Account and Settings. Settings live here, in the surface the user already has open. There is no separate options page. What a signed-out user sees first is one setting, auth.signIn in site.config.ts (Authentication). Your feature renders below either screen, so a free feature is never behind sign-in.

The differences are layout only. The popup sizes to its content. The side panel is as tall as the window, so sidepanel/main.tsx wraps the hub in a min-h-screen flex column. Keep that wrapper when you change the page.

The toolbar icon opens the popup. Chrome lists the side panel in its side panel menu. To make the icon open the side panel instead, delete entrypoints/popup/: the background already asks for that behavior (sidePanel.setPanelBehavior({ openPanelOnActionClick: true }) in background/index.ts), and a popup takes precedence only while one exists.

Popup-only product? Drop the sidepanel module (Add or remove modules). Firefox builds never include it: the API is Chromium-only, and the Firefox manifest filters the sidePanel permission.

background/index.ts opens welcome.html in a new tab when runtime.onInstalled fires with reason install. The page is bundled, so it works before any backend is configured.

The copy is in components/welcome/WelcomePage.tsx: three cards (pin the icon, open the popup, find Settings). The name and support email come from site.config.ts. To ship without a tour, delete the onInstalled block and the entrypoints/welcome/ folder.

In Claude Code, /add-surface stats page does all of this (With AI agents). By hand:

  1. Create entrypoints/stats/index.html and main.tsx, copied from entrypoints/popup/. The folder name becomes the file name: stats.html.
  2. Keep the three things every page needs:
apps/extension/entrypoints/stats/main.tsx
import "~/assets/tailwind.css";
import "~/assets/fonts.css";
import AppProviders from "@/components/AppProviders";
import PaywallOverlay from "@/components/PaywallOverlay";
ReactDOM.createRoot(document.getElementById("root")!).render(
<AppProviders>
<StatsPage />
<PaywallOverlay />
</AppProviders>,
);

AppProviders gives the page theme, toasts, and an error boundary. PaywallOverlay makes every wall render on this page too; it reads the wall the background publishes to storage, so the decision is the same on every surface (Paywall reference).

  1. Read state through the hooks in apps/extension/hooks/ (useAuth, useBilling) and call the backend through background messages. Pages never talk to Firebase directly and never hold an ID token.
  2. Open it from anywhere in the extension:
void browser.tabs.create({ url: browser.runtime.getURL("/stats.html") });

newtab, devtools, and devtools-panel are demos. They exist to show that a new-tab page and a DevTools panel read auth and plan state exactly like the popup does. They are off by default: wxt.config.ts skips the three entrypoints unless WXT_DEMO_SURFACES=true, so they are not built and not in the manifest. That is why pnpm install and pnpm dev print:

The following entrypoints have been skipped:
- devtools-panel
- devtools
- newtab

To try them, set WXT_DEMO_SURFACES=true in apps/extension/.env and restart pnpm dev. To remove them and the notice, drop the demo-newtab and demo-devtools modules; with the folders gone there is nothing to skip.

Ship a new-tab override only when it is the product. It replaces a page the user sees many times a day, and reviewers look at it closely.