How to capture a full-page screenshot of a website with an iframe
Capture the full outer page with Playwright, and learn when an iframe’s independently scrolling content needs a separate capture.
Use browser automation to capture the parent page’s full scrollable area. The iframe appears in that screenshot as rendered inside its visible embedded viewport. If you need content that scrolls inside the iframe beyond that viewport, capture the frame’s document separately and combine the images if you need one long result.
The distinction matters: a full-page screenshot expands the outer document; it does not necessarily expand a nested iframe’s own scrollable document. Whether parent-page JavaScript can inspect the iframe DOM also depends on the same-origin policy. Playwright’s screenshot guide, its frame documentation, and MDN’s same-origin policy guide describe the relevant behavior.
1. Decide what “full page” means
| What you need | Approach |
|---|---|
| The whole outer website, including the iframe as it appears on the page | Capture the parent page with full-page mode. |
| The iframe’s entire internal document, including content below its own scrollbar | Capture the iframe document or relevant frame elements separately. Combine captures if one long image is required. |
| A cross-origin iframe’s hidden DOM or content beyond what is rendered | Parent JavaScript cannot freely inspect it. Use browser automation to capture rendered output, or coordinate with the embedded page’s owner. |
First inspect the page in a browser. Look for a scrollbar inside the iframe, clipped frame edges, lazy-loaded content, and content that appears only after scrolling. Those clues determine whether one parent screenshot is sufficient.
2. Capture the outer page with Playwright
This runnable Node.js example opens the page and captures its full scrollable area. Install Playwright and its browser first:
npm install playwright
npx playwright install chromium
// screenshot.mjs
import { chromium } from 'playwright';
const url = process.argv[2];
if (!url) throw new Error('Usage: node screenshot.mjs https://example.com');
const browser = await chromium.launch({ headless: true });
try {
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
await page.goto(url, { waitUntil: 'load', timeout: 60_000 });
// Wait for the iframe element to exist when the page inserts it asynchronously.
// Change the selector to match the site, or remove this if no iframe is expected.
const iframe = page.locator('iframe').first();
if (await iframe.count()) await iframe.waitFor({ state: 'visible', timeout: 15_000 });
// If relevant content is lazy-loaded on scroll, scroll the outer page first.
await page.evaluate(async () => {
const step = Math.max(400, window.innerHeight * 0.8);
for (let y = 0; y < document.documentElement.scrollHeight; y += step) {
window.scrollTo(0, y);
await new Promise(resolve => setTimeout(resolve, 150));
}
window.scrollTo(0, 0);
});
await page.screenshot({ path: 'page.png', fullPage: true });
} finally {
await browser.close();
}
Run it with:
node screenshot.mjs https://example.com
For a page that settles slowly, wait for a specific selector or use an appropriate fixed delay after navigation. A generic network-idle wait can be unreliable on pages that keep analytics, polling, or streaming connections open. Ensure any consent or authentication steps required to see the target page have completed before capture.
3. Capture iframe content separately when needed
Playwright can locate frames and target elements inside them. Frame selection by URL or name is useful when a page has several iframes; a locator can target a specific iframe element. This example captures a visible element inside a frame:
import { chromium } from 'playwright';
const browser = await chromium.launch({ headless: true });
try {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'load' });
const frame = page.frame({ url: /embed/ });
if (!frame) throw new Error('Expected iframe was not found');
const content = frame.locator('body');
await content.waitFor({ state: 'visible', timeout: 15_000 });
await content.screenshot({ path: 'iframe-content.png' });
} finally {
await browser.close();
}
An element screenshot captures the element’s rendered bounds. For a long internally scrolling frame, this may not produce one image containing every offscreen part. If the embedded page is under your control, you can arrange for it to expand or expose the content before capturing, or capture sections as you scroll and combine them. Check the output for overlaps, missing lazy-loaded items, and sticky content repeated between sections.
Puppeteer offers the same broad workflow: navigate the browser page and request a full-page screenshot for the parent, or select and screenshot an element. See the Puppeteer screenshot guide. An element screenshot is useful for a frame or component, but it should not be assumed to reveal an iframe document’s entire internal scroll area automatically.
4. Handle same-origin and cross-origin frames
A parent page can inspect a child frame’s document only when the browser considers the documents same-origin. Origin is based on scheme, host, and port. A cross-origin parent cannot read or manipulate the iframe DOM through ordinary page JavaScript; browser automation frame APIs can still work with the rendered frame in the browser context.
If you control both parent and embedded pages, use postMessage to coordinate. For example, the parent can ask the child to prepare its content for capture, and the child can report when it is ready. Validate the message origin on both sides. Messaging provides an explicit communication channel; it does not grant unrestricted DOM access.
// Parent page: send a request to a known embedded page.
const frame = document.querySelector('#report-frame');
frame.contentWindow.postMessage({ type: 'prepare-capture' }, 'https://reports.example');
// Child page: accept only the expected parent origin.
window.addEventListener('message', event => {
if (event.origin !== 'https://www.example.com') return;
if (event.data?.type === 'prepare-capture') {
// Expand or prepare content here, then optionally post a ready message.
event.source.postMessage({ type: 'capture-ready' }, event.origin);
}
});
Replace both example origins with the real origins. Do not use a wildcard target origin when the destination is known.
5. cURL, Python, and Node.js with ScreenshotNeo
For the outer page as a rendered visitor sees it, ScreenshotNeo provides a one-request website screenshot API. Its full-page capture includes the outer page’s rendered iframe viewport; an iframe’s separately scrolling document is a distinct capture target. Use browser automation when you need to control frame navigation or combine separate internal-frame captures.
These examples save a WebP response. Replace the target URL and provide your API key. See the ScreenshotNeo API documentation for parameters and response details.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://example.com \
-o shot.webp
Python
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const bytes = new Uint8Array(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', bytes));
6. Troubleshoot missing or incomplete iframe content
| Symptom | Likely cause | What to try |
|---|---|---|
| The screenshot shows only part of the iframe | The iframe has its own internal scrollbar; parent full-page mode captures the embedded viewport. | Capture the frame’s content separately, expand it cooperatively if you control it, or capture and combine scroll sections. |
| The iframe is absent or blank | It had not loaded, needs authentication or interaction, or its content failed to render. | Wait for the iframe element and a meaningful frame selector; complete required navigation or consent steps; inspect the saved image and browser page. |
| Parent JavaScript throws a security error reading the frame | The iframe is cross-origin. | Use browser automation to capture rendered output. If both sides are yours, coordinate with validated postMessage events. |
| Images or sections are missing below the fold | Lazy loading is triggered by scrolling. | Scroll through the outer page or frame before capture and wait for the relevant images or content to appear. |
| Frame edges or content are clipped | The iframe viewport is smaller than its document or the chosen element bounds clip overflow. | Determine whether the target is the outer page or inner document; adjust the capture target or take separate captures. |
| The screenshot differs between runs | Browser, operating system, fonts, timing, or page state varies. | Keep the browser and host environment consistent, use a fixed viewport, and wait for the same content state. Playwright notes that screenshot rendering can vary across environments. |
7. Performance, reliability, and cost
- Wait only for what you need. A selector that signals the iframe or its key content is ready is often more reliable than an arbitrary long delay. Pages with ongoing network activity may never become network-idle.
- Scroll selectively. Scrolling the entire page can trigger lazy content but adds time. If only the iframe matters, scroll that frame where possible and wait for the target content.
- Keep dimensions practical. Extremely tall pages produce large images and can take longer to render and store. For huge iframe documents, section captures may be easier to validate than one very tall image.
- Make retries bounded. Retry transient navigation or load failures with a limit and a timeout. Avoid treating a blank or blocked page as a valid screenshot without checking the output.
- Stabilize the environment. Pin the browser version, viewport, and fonts when image consistency matters.
- Budget for browser operations. A self-hosted Playwright or Puppeteer flow uses your compute and maintenance time; rendering cost depends on your hosting and workload. ScreenshotNeo has a free plan with 1,000 shots per month and no card; paid plans start at $5 for 3,000 shots. Only clean shots are billed, while bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing. Each response reports page verdict and billing headers.
Or skip the browser setup
Send one request to ScreenshotNeo for the outer page screenshot. Cookie banners are accepted and removed before the shot, along with known newsletter popups and chat widgets; these steps can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed. An MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
Sign up for 1,000 free screenshots a month, with no card.
FAQ
Does a full-page screenshot include an iframe?
It includes the iframe as rendered inside its visible viewport on the parent page. Its offscreen internal document is a separate concern.
Can I capture a cross-origin iframe?
Browser automation can capture rendered output in the browser. The parent page’s ordinary JavaScript cannot freely read the cross-origin frame’s DOM.
Can postMessage bypass the same-origin policy?
No. It allows cooperating documents to exchange messages under origin checks; it does not provide unrestricted access to the other document.
Should I use full-page mode or an element screenshot?
Use full-page mode for the outer document. Use frame or element targeting when the desired output is specific to the embedded content, and verify whether internal scrolling requires multiple captures.


