How to Manage Browser Windows with Puppeteer
Learn when to use Puppeteer’s window bounds, page resize, or viewport APIs, plus how to manage tabs, contexts, popups, and cleanup.
A Puppeteer Page is a tab-like page. Its viewport is the content rendering area inside that page. A native browser window has outer position, size, and state, such as maximized or normal. Choose the API that matches the thing you need to control:
- Use
Browser.setWindowBounds()to move, maximize, restore, or set native window bounds. - Use
Page.resize()when you need a particular content area and want the browser window sized around it. - Use
Page.setViewport()for responsive layout testing or viewport emulation. - Use
Browser.pages()to inspect open pages; use browser contexts to isolate sessions.
These APIs are not interchangeable: viewport dimensions are not the outer browser-window dimensions. The examples below use the current Puppeteer API shape reflected in the official documentation; check the Puppeteer version installed in your project because APIs can evolve. See the official window management guide, Page API, and setViewport API.
1. Install Puppeteer and launch a browser
Install Puppeteer in a Node.js project, then launch a browser and create a page. This is the basic starting point for the examples that follow.
npm install puppeteer
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({ headless: false });
try {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
console.log(await page.title());
} finally {
await browser.close();
}
})();
Use headless: false when you need to see or interact with a native window. Headless automation is often enough for viewport-based rendering, but it does not provide the same visible desktop-window workflow. If another process owns the browser, connect to it using Puppeteer’s supported connection setup rather than launching a second browser.
2. Manage native browser-window position and state
The Puppeteer window-management guide uses a page created with { type: 'window' }, then reads its window ID and updates the corresponding native window bounds. The windowId matters because bounds are applied to a specific browser window.
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({ headless: false });
try {
const page = await browser.newPage({ type: 'window' });
const windowId = await page.windowId();
await browser.setWindowBounds(windowId, {
left: 80,
top: 60,
width: 1200,
height: 800,
windowState: 'normal',
});
console.log(await browser.getWindowBounds(windowId));
await browser.setWindowBounds(windowId, { windowState: 'maximized' });
// Restore the window later:
await browser.setWindowBounds(windowId, { windowState: 'normal' });
} finally {
await browser.close();
}
})();
The documented window-state example demonstrates normal and maximized states. Use getWindowBounds() when you need to inspect the current bounds or state before changing them. The browser’s title bar and other chrome affect outer dimensions, so a window width is not the same as the page’s content width.
For the API’s exact supported bounds and state fields in your installed version, consult the official window management guide. Window placement can also be constrained by the operating system, desktop environment, or headless mode.
3. Resize the browser window to a content size
Use Page.resize({ contentWidth, contentHeight }) when the target is the rendered content area. The guide recommends clearing the default viewport first when it would constrain the resize, then waiting for the resize event before measuring.
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({ headless: false });
try {
const page = await browser.newPage({ type: 'window' });
await page.setViewport(null);
const resized = new Promise(resolve => page.once('resize', resolve));
await page.resize({ contentWidth: 600, contentHeight: 400 });
await resized;
const dimensions = await page.evaluate(() => ({
innerWidth: window.innerWidth,
innerHeight: window.innerHeight,
outerWidth: window.outerWidth,
outerHeight: window.outerHeight,
}));
console.log(dimensions);
} finally {
await browser.close();
}
})();
Resize events and measurements are asynchronous: do not immediately assume that a call has finished updating the page’s inner dimensions. The outer size includes browser chrome. The official guide’s sample output showed an inner size of 600×400 and an outer size of 600×487 in its example environment; that difference is environment-specific, not a universal chrome height.
4. Set a viewport for responsive layouts and emulation
Use Page.setViewport() when your goal is to control the page’s rendering viewport. Set it before navigation when possible so the page starts at the intended dimensions.
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.setViewport({ width: 390, height: 844, deviceScaleFactor: 1 });
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
console.log(await page.evaluate(() => ({
width: window.innerWidth,
height: window.innerHeight,
})));
} finally {
await browser.close();
}
})();
The viewport is per page. The official API reference notes that changing mobile-related properties such as isMobile or hasTouch can cause a page reload. Configure those properties before navigation when avoiding a second load matters.
For example, a mobile-emulation viewport can include device scale and touch behavior:
await page.setViewport({
width: 390,
height: 844,
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
});
Use these options to test responsive rendering, not to set the outer desktop window’s screen position. If the task requires moving or maximizing a visible window, use the native-window APIs instead.
5. Open and inventory multiple pages and windows
A browser can have several Page instances. Create additional pages to open more tabs in the browser, or create a window-type page when your workflow needs a separate native window.
const first = await browser.newPage();
const second = await browser.newPage();
await Promise.all([
first.goto('https://example.com'),
second.goto('https://example.org'),
]);
const pages = await browser.pages();
console.log(`Open pages: ${pages.length}`);
for (const page of pages) {
console.log(await page.url());
}
Browser.pages() returns open pages across contexts. By default, it omits non-visible pages such as background pages; the optional includeAll flag can include those where supported by the installed API. See the Browser.pages reference and verify the signature for your installed version.
To get a page’s native window ID and then inspect or update its window, use page.windowId() with the bounds methods. Keep track of page-to-window associations in your own automation when managing more than one window; a list of pages alone does not tell you which window bounds to change.
6. Isolate sessions with browser contexts
Use a browser context when pages need independent cookies and local storage. A popup opened by a page belongs to that page’s context. Closing the context closes its pages, so context ownership is a useful cleanup boundary.
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({ headless: false });
try {
const contextA = await browser.createBrowserContext();
const contextB = await browser.createBrowserContext();
const [pageA, pageB] = await Promise.all([
contextA.newPage(),
contextB.newPage(),
]);
await Promise.all([
pageA.goto('https://example.com'),
pageB.goto('https://example.org'),
]);
// Closing a context also closes the pages created in it.
await contextA.close();
await contextB.close();
} finally {
await browser.close();
}
})();
Contexts are for session isolation, not native window sizing. See the official BrowserContext reference and browser management guide.
7. Clean up the browser you own
If your script launched the browser for this task, call browser.close() in a finally block so errors do not leave it running. If Puppeteer connected to a browser controlled by another process, browser.disconnect() detaches Puppeteer while leaving the browser and its pages running. Choose based on who owns the browser lifecycle.
try {
// Use browser and pages here.
} finally {
await browser.close(); // launched and owned by this script
// Use await browser.disconnect() when detaching from an externally owned browser.
}
8. Choosing the right API
| Goal | API | Controls | Keep in mind |
|---|---|---|---|
| Move, maximize, restore, or inspect a native window | Browser.getWindowBounds() / Browser.setWindowBounds() |
Position, outer bounds, state | Use the relevant window ID and a window-type page as needed. |
| Make the content area a specific size | Page.resize() |
Content width and height, with the window adjusted around them | Clear a constraining viewport when appropriate and await resize completion. |
| Test responsive layout or emulate a viewport | Page.setViewport() |
Page viewport dimensions and emulation properties | Some mobile or touch changes can reload the page. |
| Inspect open pages | Browser.pages() |
Page inventory | Background pages are omitted by default. |
| Separate cookies and local storage | Browser.createBrowserContext() |
Session boundary | Closing the context closes its pages; popups inherit the opener’s context. |
9. Troubleshooting common problems
The page resized, but the native window did not move
setViewport() changes the content rendering area, not the desktop window’s position. Use setWindowBounds() for native position and state, or Page.resize() when you need a content size that drives the window size.
The measured dimensions do not match the requested dimensions
Check whether you measured innerWidth/innerHeight or outerWidth/outerHeight. Browser chrome contributes to outer dimensions. When using Page.resize(), clear a constraining viewport if appropriate and wait for the resize event before reading measurements.
The requested window bounds have no effect
Confirm that you obtained the ID for the intended window and are using a visible window workflow. The operating system or browser environment can constrain placement, and headless operation is not the same as controlling a desktop window. Check the installed Puppeteer version’s window-management API.
Changing mobile emulation caused another navigation
The setViewport() API documents reload behavior for changes to properties such as isMobile and hasTouch. Set the complete viewport before calling goto() if that reload is undesirable.
A page is missing from browser.pages()
The default inventory excludes non-visible pages such as background pages. Use the optional includeAll argument if it is available in your installed Puppeteer version and you need those pages.
Closing the browser disrupts another controller
browser.close() closes the browser. If another process owns it, detach with browser.disconnect() instead. Conversely, disconnecting from a browser launched only for a short-lived script leaves that browser running, so close it when your script owns its lifecycle.
10. Performance, reliability, and cost considerations
- Set configuration early: set the viewport before navigation and batch independent page navigations with
Promise.all()when the target sites and available resources allow it. - Wait for the right condition: use a navigation wait condition appropriate to the page. Waiting for every network request to finish can be unsuitable for pages with long-lived connections; use a bounded timeout and the readiness condition your task actually needs.
- Keep sessions bounded: close pages and contexts when done. Reuse a browser process for related work when practical, while keeping unrelated sessions in separate contexts.
- Make cleanup reliable: place browser closure in
finally. For remote or externally managed browsers, disconnect instead of closing the shared process. - Account for environment variation: outer-window dimensions depend on browser chrome and the operating system; do not hard-code a universal difference between content and outer size.
- Cost: Puppeteer itself is an open-source browser automation library, but running it consumes the compute and browser resources of the machine or service hosting it. Size concurrency and timeouts to that environment; the documentation cited here does not establish a universal runtime or cost benchmark.
11. Or skip the browser setup
If the task is to get a screenshot rather than control a local browser window, ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns an image or PDF. See the ScreenshotNeo API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed; response headers identify the page verdict and billing status. Its MCP server lets AI agents use screenshot tools, and the free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up for free and get 1,000 screenshots a month with no card.
12. FAQ
Can Puppeteer control more than one browser window?
Yes. Create pages or window-type pages as needed, then use the relevant page’s window ID to inspect or change native bounds. Track which page belongs to which window in your automation.
Does setting a viewport change the physical desktop window size?
No. It sets the page’s rendering viewport. Use the window bounds APIs for native window position and state, or Page.resize() for a requested content area.
Does closing a browser context close its pages?
Yes. A context is a useful lifecycle boundary for its pages and isolated storage.
Where can I confirm which API shape my project supports?
Check the installed Puppeteer package version and consult its matching official API documentation, especially for window management and optional page-inventory arguments.


