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

Use the Frame that owns each iframe’s document, then scroll an element inside that frame. In Puppeteer, enumerate frames with page.frames() or walk mainFrame().childFrames(), identify each frame by a stable URL, name, or attribute, and call frame.locator(selector).scroll() for offset scrolling. If you need a particular item visible, obtain its element handle and call scrollIntoView(). Nested iframes require another explicit childFrames() traversal.

How Puppeteer models iframes

Puppeteer treats an iframe as a separate DOM and JavaScript context represented by a Frame object. A selector run against page or page.mainFrame() cannot automatically find elements inside an embedded document. Select the owning frame first, then query within that frame.

The page has one main frame and zero or more descendants. A descendant can itself contain child frames, so the structure is a tree rather than a flat list. The official Frame reference describes frames as the equivalent of <iframe> elements and shows recursive traversal.

Inspect every frame before scrolling

Start by printing the frame tree. This reveals URLs, names and nesting, and prevents guessing which iframe contains the scroll region.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({headless: true});
const page = await browser.newPage();
await page.goto('https://example.com/page-with-iframes', {
  waitUntil: 'networkidle2',
  timeout: 60_000
});

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

printFrame(page.mainFrame());
await browser.close();

page.frames() returns the collection of attached frames, which is convenient when you do not need to preserve the tree shape:

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

Frame names are useful clues, but do not assume they are unique or stable. Prefer a known URL fragment or an attribute on the iframe element when the site provides one.

Choose the correct frame

Match by URL

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

Match by frame name

const targetFrame = page.frames().find(frame => frame.name() === 'details-frame');

Names can be empty, duplicated or generated by the application, so treat this as a site-specific condition rather than a universal identifier.

Match the iframe element, then its content frame

When URL and name are not reliable, locate the iframe element in the parent document and ask Puppeteer for its associated frame:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const iframeElement = await page.waitForSelector('iframe[data-panel="details"]');
const targetFrame = await iframeElement.contentFrame();
if (!targetFrame) throw new Error('The iframe has not attached a document yet');

This distinction matters: the iframe element belongs to the parent DOM, while the document rendered inside it belongs to the returned Frame.

Scroll a region inside each iframe

Use a frame-scoped locator when you want to move a scrollable element by a known amount. Locator.scroll() uses mouse-wheel events and accepts offsets such as scrollTop and scrollLeft.

for (const frame of page.frames()) {
  if (!frame.url().includes('/embedded/')) continue;

  const region = frame.locator('.scroll-region');
  await region.scroll({scrollTop: 500, scrollLeft: 0});
}

The selector is evaluated in each matching frame, not in the top-level document. If a frame can contain several regions, use a more specific selector or iterate matching elements deliberately.

Locators perform action precondition checks and can retry when an element is not ready. Their viewport check is enabled by default and can be configured with the locator viewport-setting API when your workflow needs different behavior.

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

Bring a specific item into view

Offset scrolling and target revelation are different tasks. To reveal a known item, select it and call scrollIntoView() on its element handle:

const item = await targetFrame.waitForSelector('[data-id="invoice-42"]', {
  visible: true,
  timeout: 30_000
});
if (!item) throw new Error('Target item did not appear');
await item.scrollIntoView();
await item.dispose();

An action such as clicking a locator also normally ensures that the element is in the viewport. Use that behavior when the next operation is an interaction rather than a pure scroll. scrollIntoView() is the clearer choice when you only need visibility.

Traverse nested iframes

A frame’s JavaScript context does not automatically include its nested frames. Find the parent first, then inspect its childFrames():

const outer = page.frames().find(frame =>
  frame.url().includes('/outer-widget/')
);
if (!outer) throw new Error('Outer frame not found');

const inner = outer.childFrames().find(frame =>
  frame.url().includes('/inner-document/')
);
if (!inner) throw new Error('Inner frame not found');

await inner.locator('.inner-scroll-region').scroll({scrollTop: 800});

For unknown depth, use a recursive search:

function findFrame(root, predicate) {
  if (predicate(root)) return root;
  for (const child of root.childFrames()) {
    const result = findFrame(child, predicate);
    if (result) return result;
  }
  return null;
}

const frame = findFrame(page.mainFrame(), f =>
  f.url().includes('/document-to-scroll/'));
if (!frame) throw new Error('Nested target frame not found');

Complete multi-iframe example

This script discovers all matching frames, waits for each scroll region, scrolls it, and verifies that the operation completed without silently skipping a frame.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({headless: true});
try {
  const page = await browser.newPage();
  await page.goto('https://example.com/dashboard', {
    waitUntil: 'networkidle2',
    timeout: 60_000
  });

  const candidates = page.frames().filter(frame =>
    frame.url().includes('/embedded/report/')
  );
  if (candidates.length === 0) {
    throw new Error('No report iframes are attached');
  }

  for (const [index, frame] of candidates.entries()) {
    await frame.waitForSelector('.scroll-region', {timeout: 30_000});
    const region = frame.locator('.scroll-region');
    await region.scroll({scrollTop: 600, scrollLeft: 0});
    console.log(`Scrolled report frame ${index + 1}: ${frame.url()}`);
  }
} finally {
  await browser.close();
}

Replace the URL and selectors with values from your application. The example uses the current Puppeteer locator APIs; the cited official references display mixed versions (25.12.0 for Frame and page-interactions pages, 25.9.0 for the Frame.locator page), so verify that your installed package supports the methods you use.

Wait for frames and lazy content

Frames may attach after the top-level navigation, and their documents may navigate again. Synchronize on the condition you need instead of assuming that a frame exists immediately.

  • Wait for the iframe element with page.waitForSelector() before calling contentFrame().
  • After selecting a frame, wait for its target with frame.waitForSelector() or a frame wait function.
  • If the embedded application lazy-loads content, wait for the item or loading state that proves the content is present.
  • If a frame navigates or is detached, discard the old Frame reference, rediscover it and retry the operation.

Do not use a fixed delay as the only synchronization mechanism; a delay can expire before a slow frame is ready or waste time when it is already complete.

Common mistakes and fixes

The selector returns nothing

Cause: the query ran in the main frame, or the selector belongs to a different embedded document. Fix: print page.frames(), identify the owning frame, and call frame.locator() or frame.waitForSelector().

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

The iframe itself moves, but its internal panel does not

Cause: you scrolled the parent page’s iframe element, not the scroll container inside the iframe document. Fix: select the internal region through its Frame. Scroll the iframe element only when your intended result is to move the embedded rectangle within the parent page.

contentFrame() returns null

Cause: the iframe has not attached a document yet, or it was replaced. Fix: wait for the element, call contentFrame() after attachment, and reacquire it after navigation.

The frame URL does not match

Cause: redirects, query strings or an about:blank startup URL. Fix: inspect the URL after the embedded app finishes navigation, match a stable substring, or identify the iframe by a parent attribute.

A nested target is still missing

Cause: querying the outer frame does not search its child frames. Fix: call outer.childFrames() and query the returned child, repeating the traversal for deeper levels.

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

The scroll runs but the expected item remains hidden

Cause: the selected element is not the actual scroll container, or the item has not loaded. Fix: select the container for offset scrolling, wait for the item, and use scrollIntoView() when the goal is target visibility.

An API method is unavailable

Cause: the project’s Puppeteer version differs from the documentation version. Fix: check the installed package version and its matching API reference before adopting newer locator methods.

Reliability and performance considerations

  • Filter frames before performing work; scanning every frame and every descendant is unnecessary on pages with large frame trees.
  • Use stable selectors and URL fragments rather than positional indexes, which change when the page layout changes.
  • Scroll frames sequentially when actions can affect shared state. Parallel scrolling can reduce elapsed time, but only use it when the embedded applications are independent.
  • Keep explicit, bounded timeouts so a broken frame cannot hold the whole job indefinitely.
  • Capture diagnostic data—frame URL, name, selector and failure stage—when a frame disappears or a target times out.
  • Remember that wheel scrolling changes the selected element’s scroll position; it does not guarantee a particular pixel position if application code intercepts wheel events.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If your actual goal is a clean image or PDF of a page rather than interactive iframe automation, ScreenshotNeo provides a one-request screenshot API and an MCP server for AI agents. It accepts consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups and chat widgets before capture; each cleanup step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed, and response headers identify the page verdict and billing result.

For a direct capture, 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

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}`);

ScreenshotNeo also supports full-page capture with lazy images, CSS-selector element capture, dark mode, device presets and custom viewports, retina scale, PDF paper settings and page ranges, custom CSS and JavaScript, pre-capture clicks, selector waits, delay or network-idle waits, ad/tracker/request blocking, headers, cookies, user agents, authorization, timezone and geolocation, transparent backgrounds, resizing, chosen cache TTLs, signed image links, asynchronous jobs with signed webhooks, up to 100 URLs per bulk call, a usage API and an OpenAPI specification. Its parameter names are compatible with those used by other screenshot APIs, which can simplify migration.

The MCP server exposes take_screenshot, get_page_info and capture_pdf to Claude, Cursor and other MCP clients. Plans are:

Plan Price Included shots
Free $0 1,000 per month; no card
Starter $5 3,000
Growth $15 15,000
Pro $39 60,000
Scale $99 250,000
Business $249 1,000,000

Yearly billing gives two months free, and every feature is available on every plan. Create a free ScreenshotNeo account to get 1,000 screenshots a month without adding a card.

FAQ

Can I scroll all frames with one page-level call?

No. Each embedded document has its own Frame context, so locate and operate on each frame separately.

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

Should I use scroll() or scrollIntoView()?

Use scroll() for a controlled offset on a scrollable element. Use scrollIntoView() when a particular element must become visible.

Best Value
The SQL Programming Language: .
  • Used Book in Good Condition

Does a child frame inherit selectors from its parent?

No. Traverse to the child frame explicitly, then run the selector there.

What should I do after an iframe reloads?

Rediscover the frame and its target after the navigation; a previously held frame or element handle may no longer be attached.

Frequently Asked Questions

Can I scroll all frames with one page-level call?

No. Each embedded document has its own Frame context, so locate and operate on each frame separately.

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.

Should I use scroll() or scrollIntoView()?

Use scroll() for an offset on a scrollable element; use scrollIntoView() when a particular element must become visible.

Does a child frame inherit selectors from its parent?

No. Traverse to the child frame explicitly, then run the selector there.

What should I do after an iframe reloads?

Rediscover the frame and its target after navigation because old frame or element handles may be detached.

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.

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