ScreenshotNeo

BlogHow-to

How to List Frames on a Page with Puppeteer

Use page.frames() to list a Puppeteer page’s attached frames and frame.url() to read their URLs. Walk childFrames() recursively when you need the nested frame tree.

By the ScreenshotNeo team4 October 20266 min read

Use Puppeteer’s page.frames() to get every frame currently attached to a page, then call frame.url() to read each frame’s URL. For a flat list:

const frames = page.frames();
console.log(frames.map(frame => frame.url()));

If you need to preserve the parent-child nesting, start at page.mainFrame() and recursively visit each frame’s childFrames(). These methods inspect frames in one Puppeteer Page; use browser.pages() when you want browser tabs instead. See the official Page API, Frame API, and Frame.url() reference.

List every attached frame and its URL

page.frames() returns an array of the frames attached to that page when you call it. Map over the array to inspect each URL:

const frames = page.frames();

for (const [index, frame] of frames.entries()) {
  console.log(`${index}: ${frame.url()}`);
}

The main document is represented by the page’s main frame, and embedded documents appear as additional frames. A frame can also contain child frames of its own. If the target is a specific frame or nested document, use the frame objects rather than assuming the returned array communicates the hierarchy.

Runnable example: list frames in a page

Install Puppeteer with npm, save this as list-frames.js, then run node list-frames.js. The example launches the bundled browser, opens a page, waits for its load event, prints the current frame URLs, and closes the browser even if an error occurs.

const puppeteer = require('puppeteer');

async function main() {
  const browser = await puppeteer.launch({ headless: true });

  try {
    const page = await browser.newPage();
    await page.goto('https://example.com', { waitUntil: 'load' });

    for (const [index, frame] of page.frames().entries()) {
      console.log(`${index}: ${frame.url()}`);
    }
  } finally {
    await browser.close();
  }
}

main().catch(error => {
  console.error(error);
  process.exitCode = 1;
});

Setup and run:

npm install puppeteer
node list-frames.js

For a page that keeps loading resources, consider a different navigation condition such as domcontentloaded, or use a bounded navigation timeout. The right condition depends on what you need to inspect; listing frames does not require every network request to finish.

Preserve nested frame structure

A flat array is convenient, but it does not directly show which frame is a child of which parent. Traverse from the main frame and recurse through childFrames() to print the tree:

function dumpFrameTree(frame, indent = '') {
  console.log(`${indent}${frame.url()}`);

  for (const child of frame.childFrames()) {
    dumpFrameTree(child, `${indent}  `);
  }
}

dumpFrameTree(page.mainFrame());

Put this traversal after navigation in the runnable example to see nested structure. Indentation represents depth; the main frame is the root. Puppeteer’s Frame API documents this recursive pattern.

Choose between a flat list and a frame tree

Need Use What you get
All current frames, without hierarchy page.frames() An array of frame objects
A frame’s URL frame.url() The URL for that frame
Parent-child nesting page.mainFrame() and recursive childFrames() A tree rooted at the main frame
Open pages or tabs in the browser browser.pages() Page objects, not frames

Use the smallest method that answers the question. A flat list is usually enough for collecting URLs or checking whether a known URL is present. Traverse the tree when nesting, ancestry, or child order matters.

Handle pages whose frames change

Frame enumeration is a snapshot of the frames attached when you call it. Pages can attach, navigate, or detach frames later, so take a fresh snapshot after the relevant page change if you need current state. Puppeteer documents page events for frame attachment, navigation, and detachment in the Frame API.

For event-driven tracking, register listeners before the action that may change the page, then enumerate again when that action completes:

page.on('frameattached', frame => {
  console.log('Attached:', frame.url());
});

page.on('framenavigated', frame => {
  console.log('Navigated:', frame.url());
});

page.on('framedetached', frame => {
  console.log('Detached:', frame.url());
});

// Perform the action that may add, navigate, or remove frames here.
// Take a fresh snapshot after that action:
const currentUrls = page.frames().map(frame => frame.url());
console.log(currentUrls);

An event is a signal that the frame tree changed; it is not a permanent inventory. If a frame detaches while your code is inspecting it, take a new snapshot and handle the page’s current state rather than relying on a previously saved list.

Frames are not browser tabs

page.frames() only returns frames belonging to that one Page. To enumerate open pages across a browser, call browser.pages() and then inspect each page’s frames:

const pages = await browser.pages();

for (const [pageIndex, currentPage] of pages.entries()) {
  console.log(`Page ${pageIndex}`);
  for (const frame of currentPage.frames()) {
    console.log(`  ${frame.url()}`);
  }
}

The Puppeteer Browser.pages() reference notes that its default result omits non-visible pages such as background pages. This is a browser-wide page listing, a different level of structure from frames inside a page.

Common problems and fixes

Symptom Likely cause Fix
A frame URL is missing from the result The frame had not attached yet, or it detached before enumeration. Wait for the page action that creates it, then call page.frames() again. Track frame lifecycle events if the page changes dynamically.
The list has fewer entries than expected You are inspecting the wrong Page, or expecting browser tabs to appear as frames. Check the page you navigated, and use browser.pages() for pages or tabs.
A URL is empty or unexpected The frame may still be navigating or may have a URL different from the one you assumed. Inspect again after the relevant navigation and compare the current frame.url() values.
Nested frame relationships are unclear A flat array does not express the tree in the format your code needs. Start from page.mainFrame() and recursively walk childFrames().
Navigation waits too long The page may continue fetching resources after its DOM is available. Choose a navigation wait condition suited to the task, such as domcontentloaded, and set a reasonable timeout.

Performance, reliability, and resource use

Calling page.frames() gives you the current attached frames as an array; the practical cost of printing their URLs is small for ordinary pages. If you need nesting, recursive traversal visits each reachable child once. Avoid repeatedly dumping the full tree inside tight polling loops; listen for lifecycle events or enumerate after meaningful page actions.

For reliable automation, treat each enumeration as current state rather than a durable record. Pages can change while automation runs. Keep navigation waits bounded, close the browser in a finally block, and recapture the frame list after actions likely to add or remove embedded content. Puppeteer requires running a browser process, so its resource use depends on the browser and page workload; this frame-listing API has no separate per-call service fee.

Or skip the browser setup

If your goal is a screenshot rather than inspecting Puppeteer’s frame tree, ScreenshotNeo provides a website screenshot API and MCP server. One GET request can return an image or PDF. For API details and options, see the ScreenshotNeo documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

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)

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}`);
const bytes = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', bytes));

ScreenshotNeo accepts cookie and consent banners like a visitor, then removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, and failed loads are never billed. Its MCP server provides screenshot tools for AI agents. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. See ScreenshotNeo’s free sign-up to get started.

FAQ

Does page.frames() include the main frame?

It returns the frames attached to the page, including its main frame. Use page.mainFrame() when you specifically need the root frame.

How do I get all iframe URLs?

Call page.frames().map(frame => frame.url()). This gives the current URLs for frames attached at that moment.

Can I use a frame after the page navigates?

Frame objects reflect live page state and can navigate or detach. If page structure changes, enumerate again and use the current frame objects.

Does this list frames in every tab?

No. It lists frames in one page. Get pages with browser.pages(), then call frames() on each page.