How to Capture a Cross-Origin Iframe in a Puppeteer Screenshot
Puppeteer can screenshot a cross-origin iframe as rendered pixels, even when parent-page JavaScript cannot read its DOM. Learn how to wait, capture, crop, and troubleshoot it.
Yes. Navigate to the host page, wait until the embedded content is ready, then call Puppeteer’s page.screenshot(). The browser captures the iframe’s rendered pixels as part of the page even though same-origin policy prevents parent-page JavaScript from reading a cross-origin iframe’s DOM. A screenshot does not grant DOM access or bypass that policy.
This guide covers a viewport screenshot, full-page capture, a crop around the iframe, frame discovery and interaction, readiness checks, common failures, and an API alternative.
1. Capture the rendered page
Install Puppeteer in a Node.js project:
npm install puppeteer
Save this as screenshot-iframe.js. It takes a viewport screenshot of the host page after navigation and writes capture.png.
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage({
viewport: { width: 1440, height: 1000 },
deviceScaleFactor: 1,
});
await page.goto('https://example.com/page-with-iframe', {
waitUntil: 'domcontentloaded',
timeout: 60_000,
});
// Replace this with an observable, site-specific readiness check when possible.
await page.waitForSelector('iframe', { timeout: 15_000 });
await page.screenshot({ path: 'capture.png' });
} finally {
await browser.close();
}
})().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Run it with node screenshot-iframe.js. The iframe element appearing only confirms the host page inserted the element; it does not prove that a third-party application has finished rendering. Use a condition tied to the content you need wherever one is available.
2. Wait for the embedded content
There is no universal readiness signal for third-party frames. Choose a wait based on what the target page exposes and what the screenshot must show.
- Wait for the host iframe element.
page.waitForSelector('iframe')confirms the element exists, not that its content is ready. - Observe frame navigation. Inspect
page.frames()after navigation and look for the expected frame URL or name. A frame being present or navigated still may not mean a single-page app inside it has completed its own work. - Wait for a visible state or a known duration. If the page offers a reliable visible signal, use it. A fixed delay can help with a known animation or delayed render, but is brittle when network and application timing change.
- Use network-idle navigation only when it fits the page. Long polling, analytics, or persistent connections can prevent network-idle conditions from resolving. Conversely, a quiet network does not prove the specific visual content is ready.
For an iframe whose origin permits Puppeteer frame-scoped automation, you can select its frame and wait for a selector inside that frame:
const frame = page.frames().find((candidate) =>
candidate.url().startsWith('https://widget.example')
);
if (!frame) throw new Error('Expected iframe was not found');
await frame.waitForSelector('[data-render-state="ready"]', {
visible: true,
timeout: 15_000,
});
await page.screenshot({ path: 'ready.png' });
Replace the URL and selector with values appropriate to the target. Do not assume every third-party frame exposes a stable selector or permits the interaction you need. Puppeteer’s frame APIs provide automation contexts; they do not change ordinary page JavaScript’s cross-origin restrictions. See the Puppeteer Frame API.
3. Choose the screenshot area
Use the capture mode that matches the requested output. A full-page capture changes page coverage; it does not provide access to the iframe’s internal document.
| Need | Approach | Notes |
|---|---|---|
| Current visible composition | page.screenshot() |
Captures the rendered page viewport, including visible embedded content. |
| Full host page | page.screenshot({ fullPage: true }) |
Captures beyond the viewport vertically. It does not grant cross-origin DOM access. |
| Iframe box or another element | elementHandle.screenshot() |
Scrolls the element into view, then captures through the page screenshot machinery. |
| Specific rectangle | page.screenshot({ clip: { x, y, width, height } }) |
Coordinates are relative to the page viewport; check the current layout and viewport. |
To screenshot the host-page iframe element, select it from the parent document and call the element’s screenshot method. This captures the element’s rendered area; it is not a separate extraction of the cross-origin document.
const iframeElement = await page.waitForSelector('iframe#checkout', {
visible: true,
timeout: 15_000,
});
await iframeElement.screenshot({ path: 'iframe-area.png' });
If the iframe is inside a container and you want the surrounding border or label too, select that container instead. For a fixed crop, measure the element’s box and pass the coordinates as a clip:
const box = await iframeElement.boundingBox();
if (!box) throw new Error('Iframe has no visible bounding box');
await page.screenshot({
path: 'iframe-crop.png',
clip: { x: box.x, y: box.y, width: box.width, height: box.height },
});
Prefer the element screenshot when a scroll into view is appropriate. A clip is useful when you need precise viewport-relative coordinates. Layout changes, scrolling, sticky elements, and device scale can affect the region you see, so inspect the result at the chosen viewport.
4. Find and automate a frame
Puppeteer exposes frames attached to a page. Use the frame tree when you need to identify a particular embed, inspect its URL or name, or perform permitted automation in its context.
for (const frame of page.frames()) {
console.log({ url: frame.url(), name: frame.name() });
}
const hostFrame = page.mainFrame();
for (const child of hostFrame.childFrames()) {
console.log({ url: child.url(), name: child.name() });
}
Frame-scoped selectors and evaluation are useful for interaction in the selected frame when the browser and target permit it. They are separate from code executed by the parent page. Do not use parent-page iframe.contentDocument to read a cross-origin frame: under the same-origin policy, access to another origin’s document is restricted. The browser can still compose and screenshot the rendered page.
If you control both the host and embedded application, design an intentional cross-origin communication interface using postMessage(). Validate message origins and payloads in application code. This is a way for cooperating pages to exchange selected data; it is not necessary for a visual screenshot.
5. Screenshot options that matter
Puppeteer’s screenshot options include several controls relevant to iframe captures. Check the API reference for your installed version and browser pairing because defaults and supported details can vary.
| Option | Use | Consideration |
|---|---|---|
path |
Write the image to a file. | Without a path, the screenshot API returns image bytes. |
type |
Select an image format such as PNG, JPEG, or WebP where supported. | PNG is lossless; lossy formats may reduce file size. Confirm format support for your version. |
fullPage |
Capture the full page instead of only the viewport. | Does not affect frame permissions. Very tall pages can require more memory and time. |
clip |
Capture a rectangle. | Requires a valid rectangle and is useful for a bounded region. |
omitBackground |
Omit the default background for transparency where supported. | The page’s own backgrounds and rendered content still affect the result. |
captureBeyondViewport |
Control capture outside the viewport in relevant screenshot modes. | Its interaction with clipping and full-page behavior is version-sensitive; consult the reference. |
fromSurface |
Choose the browser surface used for capture. | Defaults and behavior are documented in the versioned API reference. |
See the Puppeteer screenshots guide, ScreenshotOptions reference, Page.screenshot reference, and ElementHandle.screenshot reference.
6. Troubleshoot blank, missing, or incorrect iframe captures
| Symptom | Likely cause | What to do |
|---|---|---|
| Iframe area is blank or incomplete | The screenshot ran before the embedded content rendered, or the embed itself failed. | Wait for a target-specific visual readiness signal when available. Inspect frame URLs and navigation, and check the rendered page in the same environment. |
contentDocument is null or access throws |
The iframe is cross-origin and the same-origin policy restricts parent-page DOM access. | Capture the host page’s rendered pixels. Use Puppeteer’s frame context for supported automation, or add a deliberate postMessage() interface if you control both origins. |
| The iframe is absent altogether | The embedded site may prohibit framing, for example with X-Frame-Options or related embedding policy. |
Check the browser’s console and network response. The embed owner must allow framing; a screenshot option cannot override the site’s policy. |
| Wrong crop or region | The viewport, element geometry, scroll position, or clip coordinates differ from expectations. | Set the viewport explicitly, inspect boundingBox(), and choose element capture or a clip based on the desired region. |
| Timeout waiting for network idle | The host or iframe keeps connections open or continues making requests. | Use a more specific readiness condition, such as a visible state in the relevant frame, when available. Avoid treating network silence as proof of visual readiness. |
| Frame lookup returns no match | The frame has not attached yet, the URL changed, or the matching rule is too strict. | Wait for the iframe element, then inspect page.frames(); match a stable URL prefix or frame name rather than a transient full URL. |
| Element screenshot fails or captures unexpected content | The element is detached, hidden, outside the expected layout, or changed during capture. | Wait for visibility, reselect after navigation or rerender, check its bounding box, and capture after the page reaches the desired state. |
For embedding restrictions and cross-origin behavior, see MDN’s guides to the same-origin policy and the iframe element.
7. Reliability, performance, and cost
- Make captures repeatable: Set the viewport and device scale explicitly, use a site-specific readiness check, and keep the page state stable between readiness and capture.
- Plan for third-party variation: Embedded content can load slowly, change its layout, require authentication, or refuse framing. Treat timeouts and missing frames as expected failure modes in a production workflow.
- Keep browser resources bounded: Close pages and the browser in cleanup code, set navigation and readiness timeouts, and avoid capturing an unnecessarily huge full page when a viewport or element crop meets the need.
- Choose image output intentionally: PNG preserves pixels without lossy compression; JPEG or WebP can reduce output size when those formats fit the downstream use. Image encoding is separate from frame access.
- Account for operating cost: Self-hosting means managing a browser process, compute, storage, and retries. Actual cost depends on workload and infrastructure; this guide makes no benchmark or price claim for Puppeteer.
8. Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. One GET request captures a URL as an image or PDF. For a page that embeds an iframe, pass the host page URL; the browser renders the page composition. Availability of third-party content still depends on its rendering and embedding policies.
Install Python’s HTTP client with python -m pip install requests, then run:
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com/page-with-iframe"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
For other callers, the same request can be made with cURL or Node.js:
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://example.com/page-with-iframe \
-o shot.webp
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://example.com/page-with-iframe',
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const image = Buffer.from(await res.arrayBuffer());
await require('node:fs/promises').writeFile('shot.webp', image);
See the ScreenshotNeo API documentation for request options. Cookie banners, popups, and chat widgets are removed before the shot; bot checks, blank pages, and failed loads are never billed; an MCP server lets AI agents take screenshots; and 1,000 screenshots a month are free with no card, with paid plans starting at $5 for 3,000. Sign up for free and get 1,000 screenshots a month with no card.
9. FAQ
Can Puppeteer take a screenshot of a cross-origin iframe?
Yes. Capture the host page after the browser renders the iframe. The screenshot includes visible pixels; it does not let parent-page JavaScript read the iframe document.
Why is iframe.contentDocument null?
When the frame is cross-origin, the same-origin policy restricts the parent page’s access to its document. Use a page screenshot for visual output or a permitted frame-scoped automation approach for interaction.
Does fullPage: true capture the iframe’s entire internal page?
It captures the host page’s full-page layout. It does not turn the iframe into a separately captured document or bypass its embedding and access rules.
Can I screenshot only the iframe?
Yes. Select the iframe element in the host page and call ElementHandle.screenshot(), or use a clip rectangle for a precise viewport region.
What if the website blocks being embedded?
The host page cannot display an embed the iframe’s security policy refuses to render. The embed owner must permit framing; screenshot settings cannot override that policy.


