How to Switch to a Newly Opened Tab with Puppeteer
Use Puppeteer’s popup event for known opener pages, or wait for a matching browser target when the opener is unknown.
Use the new tab’s Page object; Puppeteer does not require a separate browser-UI switch command. If a known page opens the tab, register a popup event listener before clicking. If the opener is unknown, wait for a matching browser target and convert it to a page.
1. Handle a tab opened by a known page
The popup event fires when a page opens a new tab or window and gives you the corresponding Puppeteer Page. Register the wait before the action so a fast popup cannot be missed.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
// Start listening before the click that opens the tab.
const popupPromise = page.waitForEvent('popup');
await page.click('a[target="_blank"]');
const newPage = await popupPromise;
await newPage.waitForNetworkIdle({ idleTime: 500, timeout: 30000 }).catch(() => {});
console.log('New tab URL:', await newPage.url());
console.log('Title:', await newPage.title());
await browser.close();
Use newPage for goto(), url(), locators, evaluation, screenshots and other page operations. Puppeteer sends commands to that page directly; no focus or tab-index operation is needed. See the official Page events documentation.
When your Puppeteer version uses an event emitter
Some installed versions expose event waiting through once or on. The event and ordering are the same:
const popupPromise = new Promise(resolve => page.once('popup', resolve));
await page.click('a[target="_blank"]');
const newPage = await popupPromise;
console.log(await newPage.url());
Use the API style supported by the Puppeteer version installed in your project.
2. Complete example with a controlled popup
This example creates a page, opens a popup with window.open, waits for its DOM to load, reads content and closes both pages safely.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
try {
await page.setContent(`
<!doctype html>
<button id="open">Open report</button>
<script>
document.querySelector('#open').onclick = () =>
window.open('data:text/html,<h1>Report</h1><p>Ready</p>', '_blank');
</script>
`);
const popupPromise = page.waitForEvent('popup', { timeout: 30000 });
await page.click('#open');
const reportPage = await popupPromise;
await reportPage.waitForFunction(() => document.readyState === 'complete');
const heading = await reportPage.$eval('h1', el => el.textContent);
console.log({ url: await reportPage.url(), heading });
await reportPage.close();
} finally {
await browser.close();
}
3. Wait for a matching target when the opener is unknown
Use browser.waitForTarget(predicate) when a script, extension, redirect or another component creates the tab and you cannot reliably listen on the opener page. Filter by target type and a distinguishing URL or condition.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
try {
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
const targetPromise = browser.waitForTarget(
target => target.type() === 'page' && target.url().endsWith('/report'),
{ timeout: 30000 }
);
await page.click('#open-report');
const target = await targetPromise;
const reportPage = await target.asPage();
if (!reportPage) throw new Error('The matching target is not a page');
await reportPage.bringToFront();
console.log(await reportPage.url());
} finally {
await browser.close();
}
The official browser target API documents this predicate-based wait. The target.type() === 'page' check prevents matching workers, service workers or other target types.
4. Choosing the right method
| Situation | Recommended method | Why |
|---|---|---|
| You know which page performs the click | page.waitForEvent('popup') |
Scopes the new page to that opener. |
| The opener is unknown | browser.waitForTarget(predicate) |
Matches the intended target by type and URL. |
| You need to inspect tabs that already exist | browser.pages() |
Returns a snapshot of current pages. |
browser.pages() is useful for inspection, but it is not a reliable replacement for an event wait when a page has not opened yet. Avoid selecting the last page after a click: multiple tabs can open concurrently, and list order does not identify which tab your action created. See the pages API.
5. Existing tabs and browser contexts
List pages already open
const pages = await browser.pages();
for (const [index, existingPage] of pages.entries()) {
console.log(index, await existingPage.url());
}
Keep tabs isolated with a context
const context = await browser.createBrowserContext();
const page = await context.newPage();
const popupPromise = page.waitForEvent('popup');
await page.evaluate(() => window.open('https://example.com/report', '_blank'));
const popup = await popupPromise;
console.log(await popup.url());
await context.close();
A browser context lets you close the opener and all related pages together without affecting other contexts.
6. Waiting for the right readiness state
Receiving a Page does not guarantee that the application has finished rendering. Choose a readiness condition that matches the page:
await newPage.waitForNavigation({ waitUntil: 'domcontentloaded' })when navigation is still in progress.await newPage.waitForSelector('#report')when a specific element marks readiness.await newPage.waitForNetworkIdle()for pages that finish after network activity settles.await newPage.waitForFunction(() => window.appReady === true)when the application exposes a readiness flag.
Do not wait indefinitely for network idle on pages with analytics, streaming or long polling. Prefer a selector or application-specific condition and keep a timeout.
7. Timeouts, races and failure handling
| Error or symptom | Likely cause | Fix |
|---|---|---|
TimeoutError waiting for popup |
The click did not open a tab, the selector matched the wrong element, or the popup was blocked. | Confirm the click, use a longer timeout only when justified, and verify popup permissions or user gesture requirements. |
Popup opens but URL is empty or about:blank |
The page was created before its navigation began. | Wait for a selector, navigation or URL condition on the returned page. |
| The wrong tab is selected | Code chose the last item from browser.pages(), while multiple pages opened. |
Use the opener’s popup event or a target predicate with a unique URL. |
target.asPage() returns no page |
The target is not a page target. | Filter with target.type() === 'page' before converting it. |
| Commands fail after the tab closes | The site or test closed the page. | Check page.isClosed() before follow-up work and avoid closing it in a competing cleanup path. |
| Content is missing | The script read the page before the app rendered. | Wait for the page’s stable selector or application-ready signal. |
Puppeteer waits default to 30 seconds in the documented wait APIs. Treat a missing popup as a test failure when it is required; catch the timeout when the popup is optional.
try {
const popupPromise = page.waitForEvent('popup', { timeout: 10000 });
await page.click('#optional-link');
const popup = await popupPromise;
await popup.waitForSelector('#content', { timeout: 10000 });
} catch (error) {
if (error.name === 'TimeoutError') {
console.log('No popup appeared; continuing because it is optional.');
} else {
throw error;
}
}
8. Performance and reliability checklist
- Register popup or target waits before the action that creates the tab.
- Use the narrowest predicate possible: target type plus a stable URL fragment or other identifying property.
- Reuse one browser process for multiple tasks, while creating fresh pages or contexts for isolation.
- Close pages and contexts in
finallyblocks to prevent leaked Chromium processes. - Use targeted readiness checks instead of a large fixed delay.
- Set explicit action and navigation timeouts appropriate for your environment.
- Log the matched URL and target type when diagnosing nondeterministic tests.
- Expect redirects, consent dialogs, bot checks and slow third-party resources to change timing.
9. Or skip the browser setup
If your goal is a screenshot rather than browser interaction, ScreenshotNeo returns an image or PDF from one GET request. Its capture steps can accept consent banners and remove more than 60 known consent platforms, newsletter popups and chat widgets before the shot; each step can be disabled. Bot checks, blank pages, timeouts, failed loads and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers.
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,
)
r.raise_for_status()
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}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const image = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', image));
ScreenshotNeo also provides an MCP server so Claude, Cursor and other MCP clients can call take_screenshot, get_page_info and capture_pdf. The Free plan includes 1,000 screenshots per month without a card; paid plans start at $5 for 3,000 shots, and every feature is available on every plan.
Create a free ScreenshotNeo account and get 1,000 screenshots a month with no card.
10. FAQ
Do I need to call bringToFront()?
No. Puppeteer commands target the returned Page directly. Use bringToFront() only when you want that tab visible in a headed browser.
Can I use the popup pattern with window.open()?
Yes. Register page.waitForEvent('popup') before evaluating code or clicking the control that calls window.open().
How do I identify a tab that opens after a redirect?
Wait for the target, convert it with asPage(), then wait for a stable URL, selector or application-ready condition after redirects finish.
What if several tabs open at once?
Use separate popup promises for known opener pages or a target predicate with a unique URL and target type. Do not rely on page-array position.
Can ScreenshotNeo reproduce Puppeteer interactions?
ScreenshotNeo supports options such as custom JavaScript, clicking an element, waiting for a selector or delay, custom headers and cookies, but it is intended to return captures rather than expose an interactive tab handle.


