Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

Await the value returned by browser.newPage() and keep it as your Page handle. For the top-level document, call page.mainFrame(). For an iframe, inspect page.frames(), identify the required frame by a stable URL or name, and use that Frame object for selectors and actions.

const browser = await puppeteer.launch();
const page = await browser.newPage();       // Page handle
const mainFrame = page.mainFrame();         // top-level Frame handle

const frames = page.frames();
const targetFrame = frames.find(frame => frame.url() === targetUrl);

What browser.newPage() returns

browser.newPage() is asynchronous. Awaiting it resolves to a Puppeteer Page, representing a browser tab (or an extension background page). The returned object is the handle you use for navigation, screenshots, keyboard and mouse input, cookies, network events, and page-level selectors.

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
const page = await browser.newPage();

await page.goto('https://example.com', {waitUntil: 'domcontentloaded'});
console.log(await page.title());

await browser.close();

Do not try to retrieve the page from the browser after opening it, and do not omit await. Assign the resolved value immediately so later operations use the same tab.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Opening several pages

Each call creates another Page. Store each result if you need to switch between tabs:

const checkoutPage = await browser.newPage();
const adminPage = await browser.newPage();

await checkoutPage.goto('https://shop.example/checkout');
await adminPage.goto('https://shop.example/admin');

Because both variables are independent handles, actions on one page do not change the other page’s main document.

How to get the main frame

A page always has a top-level document context. Call page.mainFrame() to obtain its Frame handle:

const page = await browser.newPage();
await page.goto('https://example.test');

const mainFrame = page.mainFrame();
console.log(mainFrame.url());

The main frame is replaced when a navigation commits, so obtain or re-check it after navigation if your code spans multiple documents. In ordinary code, Puppeteer page methods already target this frame. For example, page.$('button.save') is effectively a shortcut for searching the main document context.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

When to use a Frame instead of Page

  • Use Page for browser-tab concerns: navigation, viewport, dialogs, screenshots, PDFs, network interception, and the default (main-frame) DOM.
  • Use Frame when the element belongs to an embedded document, such as an iframe.
  • Use page.mainFrame() when the target is in the page’s own document.

How to access an iframe after opening the page

After navigation, call page.frames(). It returns the frames currently attached to the page, including the main frame and any nested iframe documents.

const page = await browser.newPage();
await page.goto('https://example.test');

for (const frame of page.frames()) {
  console.log({url: frame.url(), name: frame.name()});
}

Select the embedded frame using an identity that is stable for your application. A URL prefix, frame name, or an application-specific attribute is safer than assuming a numeric position.

Selecting by URL

const targetFrame = page.frames().find(frame =>
  frame.url().startsWith('https://widgets.example/'))
);

if (!targetFrame) {
  throw new Error('Widget frame was not attached');
}

await targetFrame.locator('button.submit').click();

Use startsWith when the iframe URL contains changing query parameters. If the application has a fixed URL, an exact comparison is appropriate.

Selecting by frame name

const paymentFrame = page.frames().find(frame => frame.name() === 'payment');
if (!paymentFrame) throw new Error('Payment frame not found');

await paymentFrame.locator('input[name="cardnumber"]').fill('4111111111111111');

A frame’s name can be empty, and names are not necessarily unique. Combine name and URL checks when more than one embedded document is present.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Walking nested frames

The frame tree starts at page.mainFrame(). A selected frame exposes its descendants through frame.childFrames():

function printTree(frame, depth = 0) {
  console.log(`${' '.repeat(depth * 2)}${frame.url()}`);
  for (const child of frame.childFrames()) {
    printTree(child, depth + 1);
  }
}

printTree(page.mainFrame());

Use the child frame as the context for operations inside a nested iframe; a selector run on the parent frame cannot cross into the child document.

Waiting for dynamically attached frames

page.frames() only reports frames attached at the moment you call it. A frame inserted by JavaScript may not exist immediately after newPage() or even immediately after goto(). Wait for an application-specific signal, then inspect the current frame tree.

await page.goto('https://example.test', {waitUntil: 'domcontentloaded'});
await page.waitForSelector('iframe[data-widget="checkout"]');

const checkoutFrame = page.frames().find(frame =>
  frame.url().includes('/checkout-widget')
);
if (!checkoutFrame) throw new Error('Checkout frame URL was not ready');

Waiting for the iframe element confirms that the host document created an iframe element, but the embedded document may still be loading. If necessary, wait for a selector inside the selected frame before interacting:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
await checkoutFrame.waitForSelector('form#checkout');
await checkoutFrame.locator('button.submit').click();

There is no universal delay that works for every application. Prefer a concrete readiness condition—an iframe element, a known URL, or a selector inside the frame—over an arbitrary timeout.

Page selectors versus frame selectors

Need Correct handle Example
Main document element Page shortcut or page.mainFrame() await page.locator('h1').textContent()
Top-level frame methods Frame await page.mainFrame().waitForSelector('.ready')
Embedded iframe element The specific Frame from page.frames() await frame.locator('button').click()
Nested iframe Child returned by frame.childFrames() const child = frame.childFrames()[0]

Calling page.locator('button') searches the main document; it does not search every iframe. Likewise, a locator created in one frame cannot be used to operate on an element in another frame.

Reliable frame lookup patterns

Use a helper that fails clearly

function findFrameByUrl(page, predicate) {
  const frame = page.frames().find(predicate);
  if (!frame) {
    throw new Error(`No matching frame. Current URLs:n${page.frames().map(f => f.url()).join('n')}`);
  }
  return frame;
}

const frame = findFrameByUrl(page, f =>
  f.url().startsWith('https://widgets.example/')
);

Do not rely on array indexes

Frame order can change when analytics, advertisements, consent tools, or application components are inserted. An expression such as page.frames()[1] is acceptable only when you fully control the page and its frame structure. URL or name identity remains more maintainable.

Keep the handle local to the operation

After a navigation or frame reload, an old frame reference may no longer represent the document you expect. Re-select the frame after the event that replaces it, then wait for the target selector again.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Common errors and fixes

“Cannot read properties of undefined” after newPage()

Cause: the promise was not awaited or the assignment was omitted.

Fix: use const page = await browser.newPage() inside an async function (or top-level await in a supported module).

The iframe is visible, but its button cannot be found

Cause: the selector is being run against the main frame or the iframe has not finished loading.

Fix: select the frame from page.frames(), then wait for a selector inside that frame before clicking.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

page.frames() has no matching URL

Cause: the frame is attached later, the URL redirects, or your comparison is too exact.

Fix: wait for the iframe or another readiness signal, log every current frame URL, and use a carefully chosen prefix or substring for known redirects.

The frame handle becomes unusable after navigation

Cause: navigation or replacement created a new document context.

Fix: wait for the navigation to finish, obtain the current frame again, and recreate locators tied to the replaced document.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Cross-origin iframe confusion

Cause: browser same-origin rules prevent evaluating the iframe’s DOM through the parent page’s JavaScript context.

Fix: use Puppeteer’s Frame APIs for the attached frame. If the frame is not attached or is blocked by the site, no selector lookup can manufacture access; diagnose the frame URL, load status, and browser console or network errors.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Performance and reliability considerations

  • Keep one browser instance when running multiple related tasks, but create a separate page when isolation between tabs is required.
  • Use waitUntil and targeted readiness selectors rather than long fixed sleeps. This reduces idle time while avoiding races.
  • Log frame URLs and names when diagnosing failures; the current tree is more useful than assumptions about frame order.
  • Close pages and the browser in cleanup code so failed tests do not leave Chromium processes running.
  • Use stable application identifiers. Third-party widgets can change URL parameters or nesting, so isolate matching logic in one helper.
try {
  const page = await browser.newPage();
  await page.goto('https://example.test');
  // page and frame work
} finally {
  await browser.close();
}

Or skip the browser setup

If your goal is a clean image or PDF rather than interactive Puppeteer control, ScreenshotNeo provides a website screenshot API and MCP server. It accepts a URL in one request and can return PNG, JPEG, WebP, or PDF. Before capture it accepts the cookie or consent banner like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response reports the page verdict and billing status in X-Page-Verdict and X-Billed headers.

For a direct request, see the ScreenshotNeo API documentation:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

The same call from Python:

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)

And Node.js:

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 also offers full-page capture with lazy images loaded, CSS-selector element capture, device presets and custom viewports, dark mode, retina scale, PDF paper and page-range controls, custom CSS and JavaScript, pre-capture clicks, hidden selectors, waits, request and resource blocking, custom headers and cookies, timezone and geolocation, transparent backgrounds, resizing, configurable caching, signed image links, asynchronous webhooks, bulk capture of up to 100 URLs per call, a usage API, an OpenAPI specification, and an MCP server with take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.

The Free plan includes 1,000 screenshots per month without a card. Paid plans start at $5 for 3,000 shots; yearly billing provides two months free, and every feature is available on every plan. Create a free ScreenshotNeo account.

Frequently Asked Questions

Can I get a Page from page.mainFrame()?

No. page.mainFrame() returns a Frame. The Page is the value returned by await browser.newPage().

Does page.frames() include the main frame?

Yes. The returned array contains the top-level frame and currently attached descendant frames.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

What should I do when an iframe has no useful URL?

Use another stable identity, such as its frame name or an application-specific readiness selector, and avoid positional indexes when the page structure can change.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.