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.
What ships
Section titled “What ships”| Page | Opens | Renders | Module |
|---|---|---|---|
popup | toolbar icon click | account and settings tabs around your feature | core |
sidepanel | Chrome’s side panel menu | the same tabs, docked, full height | sidepanel |
welcome | once, in a new tab, right after install | the three-step tour | core |
newtab | every new tab (demo, off by default) | a branded start page | demo-newtab |
devtools + devtools-panel | a panel inside DevTools (demo, off by default) | the support log, live | demo-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:
<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.
The welcome tour
Section titled “The welcome tour”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.
Add a page
Section titled “Add a page”In Claude Code, /add-surface stats page does all of this
(With AI agents). By hand:
- Create
entrypoints/stats/index.htmlandmain.tsx, copied fromentrypoints/popup/. The folder name becomes the file name:stats.html. - Keep the three things every page needs:
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).
- 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. - Open it from anywhere in the extension:
void browser.tabs.create({ url: browser.runtime.getURL("/stats.html") });Demo surfaces
Section titled “Demo surfaces”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- newtabTo 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.