Creating Popup Windows with chrome.windows
Open standalone extension windows in MV3 with chrome.windows.create type popup: sizing and positioning on the current screen, reusing a single window, focusing, tracking close, and Firefox and Safari differences.
Table of Contents
The toolbar popup closes the moment the user clicks elsewhere, which makes it the wrong surface for a chat window, a timer, a quick-capture form the user switches back and forth to, or a sign-in step that involves copying a code from email. A side panel stays open but is tied to one browser window. Sometimes what you want is a small, separate window: chrome.windows.create({ type: "popup" }) opens an extension page in a minimal window without tabs or an address bar. It persists until closed, can be positioned anywhere, and survives the user clicking away. This guide covers opening one well — sized correctly, placed on the right screen, and never duplicated. It belongs to tabs API and window management.
What a popup window is
windows.create with type: "popup" opens a window with a title bar and the page, but no tab strip, address bar or toolbar. It is a normal browser window in every other way: it has a window id, it appears in windows.getAll, it fires windows.onRemoved when closed, and its single tab has a tab id. It runs your extension page with full extension APIs, independent of any toolbar popup or side panel. Unlike the action popup, it does not close on blur, it has no maximum size beyond the screen, and nothing prevents you from opening ten of them — which is the most common mistake. Width, height, left and top are in screen pixels; on multi-monitor setups, coordinates span the virtual desktop, so positioning relative to the window the user is working in needs care.
Step-by-step: one well-placed window
1. Open the window from a user action
1// sw.js
2chrome.action.onClicked.addListener(() => openPanelWindow());
3chrome.commands.onCommand.addListener((cmd) => cmd === "open-window" && openPanelWindow());
Execution context: the service worker. Opening windows unprompted is intrusive and, in some engines, blocked by popup rules; tie it to the action, a keyboard command or a context menu item. If the action already has a toolbar popup, put an “Open in window” button inside it instead.
2. Reuse an existing window instead of opening another
1export async function openPanelWindow() {
2 const { panelWindowId } = await chrome.storage.session.get("panelWindowId");
3 if (panelWindowId) {
4 try {
5 await chrome.windows.update(panelWindowId, { focused: true, drawAttention: true });
6 return panelWindowId;
7 } catch { /* window was closed; fall through */ }
8 }
9 const win = await chrome.windows.create(await placement({
10 url: chrome.runtime.getURL("panel.html"), type: "popup", width: 420, height: 640, focused: true,
11 }));
12 await chrome.storage.session.set({ panelWindowId: win.id });
13 return win.id;
14}
Execution context: the service worker. Storing the window id in chrome.storage.session lets a restarted worker find the window it opened earlier. windows.update throws if the window no longer exists, which is the signal to create a new one. Alternatively, find it with chrome.runtime.getContexts({ contextTypes: ["TAB"], documentUrls: [panelUrl] }) (Chrome 116+), which survives even lost bookkeeping.
3. Position it relative to the user’s current window
1async function placement(opts) {
2 const current = await chrome.windows.getLastFocused({ windowTypes: ["normal"] }).catch(() => null);
3 if (!current) return opts;
4 const left = Math.max(current.left + current.width - opts.width - 24, current.left);
5 const top = current.top + 80;
6 return { ...opts, left, top };
7}
Execution context: the service worker. Placing the window near the right edge of the browser window the user is working in keeps it on the same monitor and out of the way of page content. Coordinates are in screen pixels; on multi-monitor setups left can be negative or exceed the primary screen’s width, which is fine as long as it is derived from a real window’s position. The browser clamps windows that would land off-screen.
4. Remember the user’s size and position
1chrome.windows.onBoundsChanged.addListener(async (win) => {
2 const { panelWindowId } = await chrome.storage.session.get("panelWindowId");
3 if (win.id !== panelWindowId) return;
4 await chrome.storage.local.set({ panelBounds: { width: win.width, height: win.height, left: win.left, top: win.top } });
5});
Execution context: the service worker. onBoundsChanged (Chrome 86+) fires when the user moves or resizes a window. Storing the last bounds and using them in placement next time respects the user’s arrangement. Validate stored bounds against chrome.system.display (with the system.display permission) or against the current window’s position before reusing them, because monitors get disconnected.
5. Track closing and clean up
1chrome.windows.onRemoved.addListener(async (windowId) => {
2 const { panelWindowId } = await chrome.storage.session.get("panelWindowId");
3 if (windowId === panelWindowId) await chrome.storage.session.remove("panelWindowId");
4});
Execution context: the service worker, top-level listener. Clearing the stored id when the window closes keeps open-or-focus accurate. If the window holds a live connection — a port, a WebSocket owned by the worker for it — close that too.
6. Size the page for the window
1/* panel.css */
2html, body { height: 100%; margin: 0; }
3body { display: grid; grid-template-rows: auto 1fr auto; min-width: 320px; }
4main { overflow: auto; }
Execution context: the extension page loaded in the window. Unlike the toolbar popup, the window’s size is set by windows.create, and the page should fill it and scroll internally. Design for resizing — the user can make the window larger or smaller — and test at the minimum size you allow.
7. Communicate with the rest of the extension
The window is just another extension page: it can use chrome.storage, open ports to the worker and receive storage.onChanged. If it needs to know which tab the user was on when they opened it, pass the tab id in the URL (panel.html?tab=123) or store it before opening — tabs.query({ active: true, currentWindow: true }) inside the popup window returns the window’s own tab, not the user’s page.
Common mistakes
- Opening a new window on every click. Reuse and focus the existing one.
- Querying “the active tab” from inside the window. You get the window’s own tab; pass the target tab id in.
- Fixed coordinates. They land on the wrong monitor; derive from the user’s window.
- Opening without a user action. Intrusive and sometimes blocked.
- Using a popup window where a side panel fits. For per-window, docked UI, the side panel is less disruptive.
Cross-browser variation
- Chrome / Edge:
type: "popup"windows,onBoundsChanged,getLastFocusedwithwindowTypes, andruntime.getContextsfor finding the window. - Firefox: supports
type: "popup"(and"panel", treated similarly); some window managers on Linux ignore position hints.onBoundsChangedis not available — track bounds when the window’s page unloads viawindow.screenX/Y. - Safari:
windows.createsupport is limited and popup windows may open as normal windows; test before relying on them, and fall back to opening a tab.
Verification
- Click the action twice and confirm only one window exists and the second click focuses it.
- Move and resize the window, close it, reopen it, and confirm it returns to the same place.
- Disconnect a second monitor (or edit stored bounds to off-screen values) and confirm the window still opens visibly.
- Restart the worker while the window is open and confirm the next click focuses rather than duplicates it.
FAQ
Can I make the window always on top?
No. Extensions cannot set always-on-top; the closest is focused: true and drawAttention to flash it.
Can the window have no title bar?
No. Popup windows keep the OS title bar for moving and closing.
Does a popup window count as a tab for tabs.query?
Yes — its single tab appears with windowType "popup". Filter with windowType: "normal" when you want only regular browsing tabs.
Related
- Opening and tracking extension pages in tabs — the tab equivalent of this pattern.
- Choosing between popup, side panel and tab — picking the surface.
- Restoring window state after restart — persisting window bounds.
- Tabs API and window management — the parent topic.