Playwright BrowserContexts and Pages Explained
Learn how Playwright BrowserContexts and Pages work, when to use each, and how to handle tabs, popups, isolation, and cleanup.
Short answer: a BrowserContext is an isolated browser session, while a Page is one tab or popup inside that session. The hierarchy is Browser → BrowserContext → Page. Create another Page when tabs belong to the same signed-in user. Create another BrowserContext when you need a separate user, clean session, or isolated test.
Playwright describes contexts as independent, incognito-like profiles. Contexts do not share cookies or cache, and each context can contain multiple pages. The official Pages guide states: “Each BrowserContext can have multiple pages.” Read the Pages guide and the BrowserContext API reference.
BrowserContext vs Page at a glance
| Need | Use | Reason |
|---|---|---|
| Open another tab for the same user | context.newPage() |
Pages in one context share session state. |
| Test another user or a clean session | browser.newContext() |
Contexts isolate cookies, cache, storage, and permissions. |
| Interact with a tab | Page |
Navigation, locators, clicks, assertions, and screenshots are page operations. |
| Catch a popup from a known opener | page.waitForEvent('popup') |
The event is tied to the page that opened it. |
| Observe any new tab in a context | context.waitForEvent('page') |
The event covers pages created anywhere in that context. |
Minimal setup
In Playwright Test, each test receives an isolated context and a default page fixture. With the Playwright library, create the browser, context, and page yourself:
import { chromium } from 'playwright';
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://example.com');
console.log(await page.title());
await context.close();
await browser.close();
The Browser API documents browser creation and context management. Closing a context closes all of its pages.
How BrowserContexts provide isolation
A context owns session-level state: cookies, local storage, session storage, permissions, emulation settings, and pages. Two contexts in one browser process remain separate.
import { chromium } from 'playwright';
const browser = await chromium.launch();
const alice = await browser.newContext();
const bob = await browser.newContext();
const alicePage = await alice.newPage();
const bobPage = await bob.newPage();
await alicePage.goto('https://example.com/login');
// Log Alice in; Bob's context cannot see Alice's cookies.
await bobPage.goto('https://example.com/account');
await alice.close();
await bob.close();
await browser.close();
Playwright’s isolation guide explains that “Playwright uses browser contexts to achieve Test Isolation.” See Browser contexts for the test-runner model.
Reuse state deliberately
Use storageState when a new context should start with a known login state. This copies a saved state into the new context; it does not make two contexts share changes made later.
const context = await browser.newContext({
storageState: 'playwright/.auth/user.json'
});
Keep authentication files out of source control because they can contain cookies and tokens.
Pages: tabs, windows, and popups
A Page is the unit you navigate and interact with. A context can hold many pages:
const first = await context.newPage();
const second = await context.newPage();
console.log(context.pages().length); // 2
await second.goto('https://example.com');
Pages created in the same context inherit its viewport, user agent, locale, timezone, permissions, and other context settings.
Open a new tab yourself
const tab = await context.newPage();
await tab.goto('https://example.com/docs');
Capture a popup opened by a known page
Register the event wait before the click or JavaScript action that opens the popup. This prevents a race where the popup opens before the listener is attached.
const popupPromise = page.waitForEvent('popup');
await page.getByText('open the popup').click();
const popup = await popupPromise;
await popup.waitForLoadState();
console.log(await popup.url());
You can also subscribe continuously:
page.on('popup', async popup => {
await popup.waitForLoadState();
console.log('Popup:', popup.url());
});
The Page API documents the popup event and page lifecycle.
Observe any page created in a context
const pagePromise = context.waitForEvent('page');
await page.getByRole('button', { name: 'Open report' }).click();
const reportPage = await pagePromise;
await reportPage.waitForLoadState();
Use the context event when the opener is unknown or several pages may be created. Use the page event when you know exactly which page should produce the popup.
Configuring a BrowserContext
Context options establish defaults for every page in that session. Common options include:
| Option | Purpose |
|---|---|
viewport |
Set page width and height. |
deviceScaleFactor |
Emulate a retina-style pixel ratio. |
userAgent |
Send a custom browser identity. |
locale |
Set language and regional formatting. |
timezoneId |
Emulate a timezone. |
geolocation and permissions |
Provide location and grant access to it. |
colorScheme |
Emulate light or dark preference. |
extraHTTPHeaders |
Add headers to requests. |
httpCredentials |
Supply HTTP basic-auth credentials. |
storageState |
Start with saved cookies and storage. |
baseURL |
Resolve relative URLs in navigation and locators. |
proxy |
Route context traffic through a proxy where supported by your setup. |
ignoreHTTPSErrors |
Allow invalid certificates in controlled test environments. |
recordVideo, recordHar, and tracing |
Collect diagnostics; enable only when needed because they add I/O. |
const context = await browser.newContext({
viewport: { width: 1440, height: 900 },
deviceScaleFactor: 2,
locale: 'en-GB',
timezoneId: 'Europe/London',
colorScheme: 'dark',
userAgent: 'MyTestBot/1.0',
geolocation: { latitude: 51.5072, longitude: -0.1276 },
permissions: ['geolocation'],
extraHTTPHeaders: { 'X-Test-Run': 'context-1' },
baseURL: 'https://example.com'
});
Check the current BrowserContext API for version-specific options and availability.
Context-level versus page-level settings
Prefer context settings for behavior shared by all tabs in a session. Use page methods for one tab’s navigation, DOM interaction, waiting, and screenshots. Some capabilities, such as route interception, can be configured at either level; a context route applies to every page in that context, while a page route applies only to one page.
await context.route('**/*.{png,jpg,jpeg}', route => route.abort());
await page.goto('https://example.com');
Be explicit about scope so a setting for one test tab does not unexpectedly affect another page.
Reliable page and popup workflows
- Create the browser once for a worker or process.
- Create a fresh context for each independent user or test.
- Create pages from that context.
- Register popup or page waits before the action that triggers them.
- Wait for a meaningful condition, such as a locator or response, instead of relying on arbitrary sleeps.
- Close the context in a
finallyblock. - Close the browser when the process is finished.
import { chromium } from 'playwright';
const browser = await chromium.launch();
const context = await browser.newContext();
try {
const page = await context.newPage();
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await page.getByRole('heading').first().waitFor();
} finally {
await context.close();
await browser.close();
}
Complete popup example
import { chromium } from 'playwright';
const browser = await chromium.launch();
const context = await browser.newContext({ viewport: { width: 1280, height: 800 } });
const page = await context.newPage();
try {
await page.goto('https://example.com');
const popupPromise = page.waitForEvent('popup');
await page.locator('a[target="_blank"]').first().click();
const popup = await popupPromise;
await popup.waitForLoadState('domcontentloaded');
console.log({
pagesInContext: context.pages().length,
popupUrl: popup.url(),
popupTitle: await popup.title()
});
} finally {
await context.close();
await browser.close();
}
Playwright Test fixtures
With Playwright Test, the runner creates an isolated context and page fixture for each test by default:
import { test, expect } from '@playwright/test';
test('has a title', async ({ page }) => {
await page.goto('https://example.com');
await expect(page).toHaveTitle(/Example/);
});
Create an additional page in the supplied context when the test needs another tab:
test('uses two tabs', async ({ context, page }) => {
const docs = await context.newPage();
await page.goto('https://example.com');
await docs.goto('https://playwright.dev/docs/pages');
});
Create a separate context only when the test needs a second isolated identity. See the isolation documentation for fixture behavior.
Common errors and fixes
| Error or symptom | Cause | Fix |
|---|---|---|
| Popup wait times out | The listener was registered after the click, or no popup was opened. | Create the promise before the action and verify the selector or event actually opens a new page. |
| Cookies appear in the wrong test | Tests reused one context. | Create a fresh context per isolated test or user. |
New tab is missing from context.pages() |
The page was created in another context or the code checked too early. | Use context.waitForEvent('page') and confirm the opener’s context. |
Target page, context or browser has been closed |
Cleanup ran before an awaited operation completed. | Await navigation and assertions before closing; keep cleanup in finally. |
| State is not shared between tabs | The tabs belong to different contexts. | Create both with the same context. |
| State unexpectedly persists | A saved storageState or reused context carried it forward. |
Remove the state file for a clean run or create a new context without it. |
| Navigation hangs | The page waits on long-running resources or an unreachable host. | Set an appropriate timeout, use a narrower wait condition, and inspect network activity. |
| Click opens no popup | Browser policy, a blocked event, or the site changed to same-tab navigation. | Check page.url(), listen for context pages, and inspect the link target. |
| Geolocation is denied | The context lacks a location or permission grant. | Set both geolocation and permissions: ['geolocation']. |
Performance, reliability, and cost
- Reuse the browser process: launching a browser is expensive; create and close contexts for isolation while keeping the browser alive when practical.
- Limit pages: each page consumes memory and may load network resources. Close pages you no longer need.
- Use the narrowest wait: waiting for a specific locator or response is usually more deterministic than fixed delays.
- Control diagnostics: tracing, video, HAR recording, and large downloads add disk and CPU work. Enable them for failed runs or targeted debugging.
- Parallelism: separate contexts are a useful unit for parallel users, but set concurrency according to available CPU, memory, and the target site’s limits.
- Cleanup: always close contexts and browsers so temporary profiles, pages, and connections are released.
- Retries: retry only transient navigation or network failures. Repeating a state-changing action can create duplicate data.
Or skip the browser setup
If your goal is a clean website screenshot rather than browser automation, ScreenshotNeo provides a single HTTP request for PNG, JPEG, WebP, or PDF output. Its capture process accepts cookie and consent banners like a visitor, then removes more than 60 known consent platforms, newsletter popups, and chat widgets before the shot. Each step can be turned off.
Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed. The response reports the result with X-Page-Verdict and X-Billed headers. ScreenshotNeo also provides an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
See the ScreenshotNeo API documentation for all options.
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}`);
There are 1,000 screenshots per month free with no card. Paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
FAQ
Can one BrowserContext contain multiple pages?
Yes. Use context.newPage() and inspect them with context.pages().
Do pages in one context share cookies?
Yes. Pages in the same context use that context’s session state.
Do separate contexts share cache?
No. Contexts are isolated, incognito-like sessions.
Should I use a Page event or a Context event for popups?
Use the Page event when the opener is known. Use the Context event to observe any page created in the session.
What closes pages automatically?
Closing a BrowserContext closes all pages it owns. Closing the browser closes its remaining contexts.
Is a new Page the same as a new user session?
No. A new Page is another tab in the same session. A new BrowserContext is the boundary for a separate session.
Key takeaways
- Think
Browser → BrowserContext → Page. - Use pages for tabs that share identity and state.
- Use contexts for separate users, clean tests, and isolation.
- Register popup and page waits before the action that creates them.
- Close contexts explicitly and browsers when the process ends.


