ScreenshotNeo

BlogHow-to

How to Find Child Frames in Puppeteer

Find direct child frames, traverse nested iframes, and query the right Puppeteer frame with runnable JavaScript examples and troubleshooting tips.

By the ScreenshotNeo team4 October 20268 min read

Use page.mainFrame().childFrames() to get the main frame’s immediate child frames. To find nested descendants, recursively visit each child frame. To get a flat list of every frame currently attached to the page, use page.frames().

const directChildren = page.mainFrame().childFrames();
const allAttachedFrames = page.frames();

function collectDescendants(frame) {
  return frame.childFrames().flatMap(child => [child, ...collectDescendants(child)]);
}

const nestedDescendants = collectDescendants(page.mainFrame());

This guide shows how to choose the right scope, identify a frame, query inside it, and handle a frame tree that changes during navigation. For the current method details, see Puppeteer’s Frame reference, childFrames() reference, and Page reference.

1. Choose direct children, descendants, or all frames

Need Use What it returns
Immediate children of the main frame page.mainFrame().childFrames() One level of child frames
Every descendant beneath the main frame Recursively call childFrames() Nested frames at every depth, excluding the starting main frame
A flat page-wide list page.frames() All currently attached frames, including the main frame
The parent of a known frame frame.parentFrame() The parent frame, or null for the main or a detached frame

Use the smallest scope that answers the question. Direct children are simplest when the iframe is embedded directly in the main document. Recursion is needed if a child iframe contains another iframe. Page-wide enumeration is convenient when you need to search all attached frames without starting from a particular parent.

2. Get the main frame’s direct child frames

childFrames() returns an array of a frame’s immediate children. This runnable example opens a page, waits for navigation, and prints the URL of each direct child:

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: true });
  try {
    const page = await browser.newPage();
    await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });

    const children = page.mainFrame().childFrames();
    for (const frame of children) {
      console.log(frame.url());
    }
  } finally {
    await browser.close();
  }
})();

Replace the example URL with a page that contains frames. A page without iframes returns an empty array. The snippet logs frame URLs, which can help with discovery, but URLs are not guaranteed to be unique or stable identifiers.

3. Traverse nested frames recursively

A child frame can itself have children. Walk the frame tree if you need every descendant, regardless of nesting depth:

function collectDescendants(frame) {
  return frame.childFrames().flatMap(child => [child, ...collectDescendants(child)]);
}

const main = page.mainFrame();
const descendants = collectDescendants(main);

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

The recursive function includes each child before visiting its descendants. It excludes the frame passed to it; because the starting frame is the main frame, the result contains descendants but not the main frame. To include the main frame too, use [main, ...collectDescendants(main)].

This is a depth-first traversal. It works for ordinary finite frame trees. If your page creates frames continuously, take a snapshot when you need it and refresh it after relevant lifecycle changes rather than assuming the array remains current.

4. Enumerate all attached frames with page.frames()

For a flat list, use page.frames(). This includes the main frame along with currently attached child frames:

const frames = page.frames();

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

This is useful when you want to search or inspect every attached frame and do not need to preserve its parent-child hierarchy. If you need nesting relationships, traverse from page.mainFrame() with childFrames().

5. Find a frame by its iframe element name

The Frame reference demonstrates one way to associate a frame with its embedding element: inspect each frame’s frameElement() and read the element’s name attribute. Then use the matching Frame object for frame-scoped operations.

async function findFrameByName(page, wantedName) {
  for (const frame of page.frames()) {
    // The main frame has no embedding iframe element.
    try {
      const element = await frame.frameElement();
      const name = await element.evaluate(el => el.getAttribute('name'));
      if (name === wantedName) return frame;
    } catch {
      // The frame may have detached while it was being inspected.
    }
  }
  return null;
}

const checkoutFrame = await findFrameByName(page, 'checkout');
if (!checkoutFrame) {
  throw new Error('Could not find the checkout frame');
}

const heading = await checkoutFrame.$eval('h1', el => el.textContent.trim());
console.log(heading);

Frame names are author-controlled and may be empty or duplicated, so use another distinguishing property when the page has multiple frames with the same name. A frame may also detach between enumeration and inspection; the example catches that case and continues. If you know a stable selector for the iframe element, locating the element and resolving its content frame can be a more direct strategy; refresh the reference if the page replaces that iframe.

6. Query and evaluate in the target frame

Once you have a Frame object, use its frame-scoped methods, such as frame.$eval() or frame.evaluate(), to work with that frame’s document:

const frame = page.frames().find(candidate => candidate.url().includes('/embedded/'));
if (!frame) throw new Error('Embedded frame not found');

const title = await frame.evaluate(() => document.title);
const buttonText = await frame.$eval('button', button => button.textContent.trim());
console.log({ title, buttonText });

Do not assume evaluation in the main frame can inspect a child frame’s document. Puppeteer’s Frame documentation explains that JavaScript executed in a frame does not affect frames inside that ambient frame. Select the target Frame and run the query there. Cross-origin iframe restrictions in page JavaScript do not prevent Puppeteer’s frame-scoped API from targeting an attached frame, but the target still needs to be present and its document ready for the operation you perform.

7. Refresh frame references when the page changes

Frames attach, navigate, and detach as a page runs. A frame list or Frame reference describes the current frame tree at the time you obtained it. If an application adds or replaces an iframe, enumerate again after the relevant action or navigation.

await page.click('button.open-widget');
await page.waitForFrame(frame => frame.url().includes('/widget'));

const widgetFrame = page.frames().find(frame => frame.url().includes('/widget'));
if (!widgetFrame) throw new Error('Widget frame was not attached');

When the page has a known iframe selector, waiting for the iframe element to appear before resolving its content frame can also make the sequence more deterministic. Avoid retaining a frame reference across an action that may remove or replace that frame. parentFrame() returns null for the main frame and for detached frames, so a null parent can indicate either case; compare against page.mainFrame() when you need to distinguish the main frame.

8. Troubleshooting

Symptom Likely cause Fix
childFrames() returns an empty array The iframe has not attached yet, or the page has no direct child frames. Wait for the page action or navigation that creates the iframe, then inspect again. Use page.frames() to see all frames currently attached.
A nested iframe is missing You inspected only the main frame’s immediate children. Recursively visit each child’s childFrames(), or search the flat result from page.frames().
A selector works in the main page but not in the iframe The selector is being evaluated in the wrong document. Find the iframe’s Frame object and use frame.$eval() or frame.evaluate().
A previously found frame no longer works The frame navigated or detached and was replaced. Re-enumerate after the page change and locate the current frame again.
The frame lookup returns the wrong match URLs or names are duplicated, blank, or changed by navigation. Use additional identifying information, such as the iframe element’s attributes or a stable URL path, and verify the match before acting.
frameElement() inspection fails intermittently The frame detached between listing it and inspecting its embedding element. Catch the detached-frame error, skip that entry, and retry enumeration if the target is still expected.
Evaluation finds no element The target document has not rendered the element, or the selector is wrong. Wait for the selector in the target frame when appropriate, check the selector against that frame’s DOM, and confirm the frame has finished navigating.

9. Performance, reliability, and cost

Frame enumeration is local to the Puppeteer page and does not require a separate network request. For ordinary pages, choosing between a short child traversal and page.frames() is usually a clarity decision. Avoid repeatedly querying every frame inside tight polling loops; wait for the page event or selector that signals the target is ready, then enumerate once.

For reliability, treat the frame tree as changing state: wait for attachment when expected, verify the frame you found, and reacquire it after navigation or replacement. Frame URLs are helpful diagnostics, not permanent IDs.

Running Puppeteer requires a browser process and the resources to load the page. If your task is simply to produce a screenshot rather than inspect iframe content, ScreenshotNeo offers a screenshot API and MCP server; its API supports browser capture options, and only clean shots are billed. The cost of a local Puppeteer workflow depends on where and how you run the browser. ScreenshotNeo’s listed plans are free for 1,000 screenshots per month, then $5 for 3,000 on Starter, $15 for 15,000 on Growth, $39 for 60,000 on Pro, $99 for 250,000 on Scale, and $249 for 1,000,000 on Business; yearly billing gives two months free. Every feature is available on every plan.

10. Or skip the browser setup

If your goal is a screenshot rather than frame inspection, use ScreenshotNeo’s one-call API. See the ScreenshotNeo API documentation for request 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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await require('node:fs/promises').writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
  • Cookie banners are accepted like a visitor and removed along with known consent platforms, newsletter popups, and chat widgets before the shot; each cleanup step can be turned off.
  • Bot checks, blank pages, failed loads, timeouts, and cache hits are never billed. Response headers report the page verdict and billing status.
  • An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for AI agents and MCP clients.
  • 1,000 screenshots per month are free with no card; paid plans start at $5 for 3,000.

Create a free ScreenshotNeo account to get 1,000 screenshots a month with no card.

11. Frequently asked questions

Does page.frames() include the main frame?

Yes. It returns all frames currently attached to the page, including the main frame.

Does childFrames() include grandchildren?

No. It returns only immediate children. Call it recursively to collect deeper descendants.

Can I use a Frame after navigation?

A frame can navigate while remaining part of the page, but navigation can change its URL and document. If the iframe was removed or replaced, reacquire the current Frame reference before querying.

How do I tell whether a frame is the main frame?

Compare it with page.mainFrame(). The main frame’s parentFrame() is null, as is a detached frame’s.