ScreenshotNeo

BlogHow-to

Puppeteer Screenshot of a Page with an iframe: Capture Embedded Content

Capture a page with an iframe in Puppeteer, wait for the embed to render, and choose between a full-page screenshot and an iframe-only image.

By the ScreenshotNeo team4 October 20268 min read

To capture embedded content with Puppeteer, wait until the iframe has rendered and call page.screenshot() on the containing page. A page-level screenshot captures what the browser rendered, including visible iframe content. Use Puppeteer’s frame tree when you need to inspect or interact with the iframe DOM; those are separate tasks. [Puppeteer screenshot guide](https://pptr.dev/guides/screenshots) · [Frame API](https://pptr.dev/api/puppeteer.frame) · [Page.screenshot() API](https://pptr.dev/api/puppeteer.page.screenshot)

The tricky part is readiness: an iframe can exist before its useful content has loaded. Wait for a target-specific selector or application-ready signal inside the frame when possible. A generic body check only confirms that a document exists; it does not prove that the embed is visually complete.

1. Capture the whole page after the iframe is ready

Install Puppeteer with npm install puppeteer. Save this as screenshot.mjs and run node screenshot.mjs https://example.com. Replace the example URL and selector with a page and a stable selector from the embedded application.

import puppeteer from 'puppeteer';

const url = process.argv[2];
if (!url) throw new Error('Usage: node screenshot.mjs <url>');

const browser = await puppeteer.launch({ headless: true });
try {
  const page = await browser.newPage({
    viewport: { width: 1440, height: 1000 },
    deviceScaleFactor: 1,
  });

  await page.goto(url, { waitUntil: 'networkidle2', timeout: 60_000 });

  const iframeHandle = await page.waitForSelector('iframe', { timeout: 15_000 });
  const frame = await iframeHandle.contentFrame();
  if (!frame) throw new Error('iframe element has no attached frame');

  // Prefer a meaningful, stable selector from the embedded app.
  await frame.waitForSelector('[data-app-ready="true"]', { timeout: 30_000 });

  await page.screenshot({ path: 'page-with-iframe.png', fullPage: true });
  console.log('Saved page-with-iframe.png');
} finally {
  await browser.close();
}

networkidle2 is a useful navigation wait, and appears in Puppeteer’s screenshot guide example, but it is not a guarantee that every third-party embed has finished rendering. Some pages keep network connections open, while others render later after navigation has gone idle. [Puppeteer screenshot guide](https://pptr.dev/guides/screenshots)

Use a readiness signal that matches the embed

  • Known app selector: wait for a stable element that appears after the embed finishes its initial render.
  • Known text or state: use frame.waitForFunction() to check for a specific state exposed by the embedded page.
  • Only checking presence: await frame.waitForSelector('body') is a basic fallback, not proof that charts, images, or other async content are ready.
  • Fixed delay: use a short delay only when the embed offers no reliable signal; it can be either wasteful or too short.

For example, if the embed exposes a stable heading, replace the app-specific wait with await frame.waitForSelector('h1'). Choose a selector that signals useful rendered content, not merely the existence of the iframe document.

2. Find and inspect the right iframe

Use page.frames() or traverse from page.mainFrame() when the page has multiple or nested frames, or when you need to read or interact with content inside one. Puppeteer represents DOM frames with Frame objects; the page’s frame tree is accessible through mainFrame() and each frame’s childFrames(). [Puppeteer Frame API](https://pptr.dev/api/puppeteer.frame)

const frames = page.frames();
for (const candidate of frames) {
  console.log(candidate.url());
}

const targetFrame = page.frames().find(frame =>
  frame.url().includes('embed.example')
);
if (!targetFrame) throw new Error('Expected embed frame was not found');

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

Frame URLs can change during redirects or app navigation. If matching by URL, inspect the frame tree after navigation and account for the URL the embed actually uses. For deeply nested embeds, search recursively through childFrames().

Nested frame traversal

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

const allFrames = [page.mainFrame(), ...descendants(page.mainFrame())];
for (const frame of allFrames) {
  console.log(frame.url());
}

3. Capture only the iframe region

If the desired output is just the embedded rectangle, screenshot the iframe element handle rather than the full page. This captures the element’s visible box. Puppeteer’s screenshot guide documents element screenshots and notes that ElementHandle.screenshot() attempts to scroll a hidden element into view. [Puppeteer screenshot guide](https://pptr.dev/guides/screenshots)

const iframeHandle = await page.waitForSelector('iframe', { visible: true });
const frame = await iframeHandle.contentFrame();
if (!frame) throw new Error('iframe element has no attached frame');
await frame.waitForSelector('[data-app-ready="true"]', { timeout: 30_000 });
await iframeHandle.screenshot({ path: 'iframe-region.png' });

The screenshot includes the iframe’s rendered box, not a separate image produced from its internal DOM. If the iframe element is clipped by its parent, off-screen, or too small, the resulting region reflects that layout. Use fullPage: true on page.screenshot() when you need the full containing page instead.

4. Choose screenshot options

Option When to use it
fullPage: true Capture the containing page beyond the viewport, including the iframe where it appears in the page layout.
path: 'page.png' Write the image to a file. Puppeteer infers the image type from the extension when supported.
type: 'png' | 'jpeg' | 'webp' Select an output image format where supported by the installed Puppeteer version.
quality: 0–100 Set lossy image quality for JPEG or WebP; it does not apply to PNG.
omitBackground: true Hide the default white background to allow transparency where the capture supports it.
clip Capture a specific page rectangle when a precise crop is needed.
captureBeyondViewport Control beyond-viewport capture behavior when using clipping; check the option’s documented defaults.
encoding: 'base64' Return a base64 string instead of binary screenshot bytes.
optimizeForSpeed Request speed-oriented capture behavior when a modest output tradeoff is acceptable.

See Puppeteer’s current [ScreenshotOptions reference](https://pptr.dev/api/puppeteer.screenshotoptions) for the version you use. For a straightforward page image, { path: 'page.png', fullPage: true } is usually enough. Full-page output can be very tall; consider a viewport capture or a clip if you only need the embed’s location.

5. Handle timing, navigation, and embeds carefully

  1. Navigate to the host page and check the response if the page may return an error status.
  2. Wait for the iframe element to appear and become visible.
  3. Get its frame with contentFrame(), then wait for an embed-specific readiness signal.
  4. If the frame navigates as part of initialization, wait for the expected frame URL or content after that navigation.
  5. Take the page or element screenshot, then close the browser in a finally block.

Some embeds load content after user interaction, require authentication, or behave differently in automation. A frame being attached does not mean its application has finished rendering. Cross-origin content is not a universal obstacle to a browser screenshot of the rendered page, but DOM inspection and interaction depend on the frame being available and the actual site, browser, and authentication state. The Puppeteer documentation describes frame access and screenshots; it does not establish a workaround that applies to every embed. Validate against the target environment. [Frame API](https://pptr.dev/api/puppeteer.frame) · [Page.screenshot() API](https://pptr.dev/api/puppeteer.page.screenshot)

6. Common problems and fixes

Symptom Likely cause Fix
Screenshot shows an empty iframe The capture ran before the embedded app rendered, or the embed did not load. Wait for a meaningful selector inside the frame; check frame URL and page errors; confirm the embed loads in the same browser and auth state.
contentFrame() returns null The iframe has not attached a browsing context, or it was replaced/detached. Wait for the iframe to appear, retrieve its handle after navigation, and check that it remains attached.
Selector timeout inside the frame Wrong frame selected, selector is stale, content is delayed, or the application failed. Log page.frames().map(f => f.url()), inspect the frame’s rendered state, and use the correct stable selector and timeout.
Embed is missing from a full-page capture It is lazy-loaded below the fold or appears only after scrolling or interaction. Scroll the page or element into view to trigger loading, wait for the app-ready state, then capture. Consider element capture if only that region matters.
Capture hangs at networkidle2 The page maintains long-lived network activity. Use waitUntil: 'domcontentloaded' or another suitable navigation condition, then wait explicitly for the iframe’s readiness signal.
Image is clipped or unexpectedly tall Full-page layout, iframe dimensions, or capture boundaries differ from expectation. Set a deliberate viewport; use element screenshot or a documented clip; inspect the page layout at capture time.
Screenshot fails after a frame is replaced The stored element or frame handle refers to a detached frame. Re-query the iframe and reacquire its frame after the replacement/navigation.
Embedded page differs from a normal browser Authentication, site policy, bot checks, or browser-dependent behavior changes what is rendered. Reproduce the required session and browser conditions where permitted; confirm the embed’s own policy and do not assume a generic bypass exists.

7. Performance, reliability, and cost

Wait for the narrowest reliable readiness signal: waiting for every network request can be slow or never finish on pages with persistent connections, while fixed sleeps make captures inconsistent. Reuse a browser process for batches of captures when appropriate, but create a fresh page or context when isolation between sessions matters. Set explicit navigation and selector timeouts, close pages and browsers reliably, and record the URL, frame URLs, and failure stage when diagnosing intermittent output.

Full-page screenshots use more time and memory than a viewport or iframe-element capture, especially on long pages or high device scale factors. Keep the viewport and output dimensions close to what the consuming workflow needs. Puppeteer itself is an open-source browser automation library; operating cost depends on where Chromium runs and the compute, storage, and bandwidth used. This guide makes no benchmark or fixed cost claim.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. A single request captures a URL as PNG, JPEG, WebP, or PDF. The endpoint takes a URL, so it captures the page as rendered; it does not provide a selector to target a specific iframe’s internal DOM. For a page-level capture:

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}`);
  • Cookie banners, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off.
  • Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are never billed; response headers report the page verdict and billing status.
  • An MCP server lets AI agents, including Claude, Cursor, and any MCP client, take screenshots.
  • 1,000 screenshots per month are free with no card; paid plans start at $5 for 3,000. Every feature is on every plan.

Get 1,000 free screenshots a month with no card.

FAQ

Does Puppeteer screenshot the iframe automatically?

A screenshot of the containing page captures the rendered page, including visible iframe content. Wait for the embed to be ready first.

Can I read iframe content if it is cross-origin?

Use Puppeteer’s frame APIs to inspect the actual frame in the browser. Behavior depends on the site and runtime; the documentation does not promise a universal cross-origin workaround.

Should I screenshot the iframe element or the whole page?

Use the iframe element handle for just its rectangular region. Use page.screenshot() for the containing page and its surrounding context.

Does waiting for networkidle2 guarantee the embed is ready?

No. It is a navigation wait condition, not a signal that a particular embedded application has finished rendering. Wait for application-specific content.