ScreenshotNeo

BlogHow-to

How to Capture an Embedded iframe Screenshot

Capture an embedded iframe manually or automate it with Playwright, Chrome DevTools Protocol, and ScreenshotNeo—including cross-origin frames.

By the ScreenshotNeo team30 September 20269 min read

How to Capture an Embedded iframe Screenshot

Use a browser screenshot operation to capture an iframe’s rendered pixels. For a one-off image, capture the browser viewport and crop it. For repeatable automation, use Playwright to wait for the frame and screenshot either the iframe element or a locator inside it. A cross-origin iframe can restrict JavaScript DOM access, but the browser can still capture what it renders on screen.

Choose the right method

Method Best for What it captures Main limitation
Browser or operating-system capture One-off screenshots Visible viewport, then crop to the iframe Manual and difficult to reproduce exactly
Playwright Repeatable tests and jobs Iframe element, content locator, or full page Requires browser setup and stable selectors
Chrome DevTools Protocol Chromium automation and precise clipping Page pixels or a clip rectangle Chromium-specific protocol details
Screen Capture API User-selected screen sharing or recording A permissioned MediaStream It prompts the user and is not a still-image API by default

Playwright documents frame locators and page and locator screenshots in its frame and screenshot guides. The Chrome DevTools Protocol documents Page.captureScreenshot, including clipping. The web platform’s same-origin rules are described by MDN.

Capture an iframe with Playwright

The following Node.js script loads a page, waits for the iframe, captures the iframe’s rendered box, and then captures an element inside the frame. Replace the URL and selectors with those from your page.

A repeatable iframe capture waits for the embedded content, then records the pixels the browser rendered.
A repeatable iframe capture waits for the embedded content, then records the pixels the browser rendered.
import { chromium } from 'playwright';

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

await page.goto('https://example.com/page-with-iframe', {
  waitUntil: 'domcontentloaded'
});

const iframe = page.locator('#my-iframe');
await iframe.waitFor({ state: 'visible' });

// Capture the complete visible iframe rectangle as rendered by the browser.
await iframe.screenshot({ path: 'iframe.png' });

// If you need content inside the frame, use frameLocator.
const frame = page.frameLocator('#my-iframe');
const chart = frame.locator('[data-testid="chart"]');
await chart.waitFor({ state: 'visible' });
await chart.screenshot({ path: 'chart.png' });

await browser.close();

Install and run it with:

npm install playwright
npx playwright install chromium
node capture-iframe.mjs

locator.screenshot() captures the element’s visible bounding box. If the iframe itself is larger than the viewport, capture the page after setting the desired scroll position, or use a clip rectangle with the iframe’s bounding box. An iframe is a replaced element in the parent page, so the iframe locator is often the most reliable way to capture exactly what the parent renders.

Wait for the frame’s application, not only the parent page

page.goto() can finish while a third-party frame is still loading data. Wait for a stable selector inside the frame, a known loading indicator to disappear, or a short application-specific readiness condition.

const frame = page.frameLocator('iframe[data-widget="report"]');
await frame.locator('[data-ready="true"]').waitFor({ state: 'visible', timeout: 30000 });
await frame.locator('.report-canvas').screenshot({ path: 'report.png' });

Do not rely on a fixed delay when a deterministic selector is available. If the embedded service has no stable marker, combine a bounded delay with a network or UI condition and record failures for review.

Capture the entire page region containing the iframe

const box = await iframe.boundingBox();
if (!box) throw new Error('Iframe is not visible or has no layout box');

await page.screenshot({
  path: 'iframe-region.png',
  clip: box
});

A page clip is useful when you need the iframe plus pixels painted by overlays or when an element screenshot does not include the surrounding composition you need. Ensure the frame is in the intended scroll position before measuring its box.

Python Playwright example

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=True)
    page = browser.new_page(viewport={"width": 1440, "height": 1000}, device_scale_factor=1)
    page.goto("https://example.com/page-with-iframe", wait_until="domcontentloaded")

    iframe = page.locator("#my-iframe")
    iframe.wait_for(state="visible")
    iframe.screenshot(path="iframe.png")

    frame = page.frame_locator("#my-iframe")
    chart = frame.locator('[data-testid="chart"]')
    chart.wait_for(state="visible")
    chart.screenshot(path="chart.png")

    browser.close()
pip install playwright
playwright install chromium

Handling same-origin and cross-origin iframes

The same-origin policy defines an origin by scheme, host, and port. A parent page cannot generally read a cross-origin frame’s DOM through iframe.contentDocument. CORS headers do not grant general DOM access. When two documents intentionally communicate, use postMessage with origin checks.

That restriction concerns script-level inspection. A browser screenshot is a rendering operation: Playwright or DevTools can capture the visible pixels without your page script reading the frame’s document. For a cross-origin frame:

  1. Make sure the iframe is visible and has non-zero dimensions.
  2. Wait for a visual readiness signal that is available to your automation context.
  3. Capture the iframe element or the parent page clip.
  4. Use a fixed viewport, device scale factor, and scroll position when pixel consistency matters.

Some embedded services prevent framing with X-Frame-Options or Content-Security-Policy: frame-ancestors. In that case, the frame may never render; a screenshot tool cannot capture pixels that the browser refused to display.

One-off capture in a browser

  1. Open the page and wait until the embedded content is visibly complete.
  2. Use the browser’s element or full-page capture feature when your installed version provides it, or capture the viewport with the operating system.
  3. Crop to the iframe’s visible rectangle.
  4. Record the viewport size and zoom if someone else must reproduce the image.

Chrome DevTools’ Application > Frames view lists the top frame and nested frames and shows each frame’s URL and origin. Menu names and shortcuts vary by browser version, so treat the DevTools view as an inspection aid rather than a portable automation contract.

Capture with Chrome DevTools Protocol

For Chromium automation, Page.captureScreenshot returns base64-encoded image data and accepts a clip rectangle. The protocol is versioned with Chromium, so match the method’s behavior to the browser you deploy.

import { chromium } from 'playwright';
import fs from 'node:fs/promises';

const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
await page.goto('https://example.com/page-with-iframe', { waitUntil: 'networkidle' });

const iframe = page.locator('#my-iframe');
await iframe.waitFor({ state: 'visible' });
const box = await iframe.boundingBox();
if (!box) throw new Error('No iframe bounding box');

const cdp = await page.context().newCDPSession(page);
const result = await cdp.send('Page.captureScreenshot', {
  format: 'png',
  fromSurface: true,
  captureBeyondViewport: false,
  clip: { x: box.x, y: box.y, width: box.width, height: box.height, scale: 1 }
});
await fs.writeFile('iframe-cdp.png', Buffer.from(result.data, 'base64'));
await browser.close();

Why the Screen Capture API is different

navigator.mediaDevices.getDisplayMedia() asks a user to select a screen, window, or tab and returns a live MediaStream. It requires a secure context and user permission, and browser support and Permissions Policy constraints vary. Use it for sharing or recording a selected surface; use Playwright or DevTools for automated still screenshots.

Iframe-specific options and edge cases

  • Nested frames: chain frame locators, such as page.frameLocator('#outer').frameLocator('#inner').
  • Lazy content: scroll the frame or its parent into view before waiting for images or charts.
  • Animations: disable animations with injected CSS or wait for a stable state to avoid inconsistent pixels.
  • Responsive layouts: set the viewport before navigation; changing it later can reflow the embedded app.
  • Browser zoom: keep zoom at 100% when comparing images across runs.
  • Fixed overlays: cookie banners, chat widgets, and parent-page overlays can cover the frame even when the frame itself is ready.
  • Canvas and video: rendering can depend on GPU, codecs, timing, and cross-origin media permissions.
  • Authentication: authenticate in the correct browsing context. Parent-page cookies may not be sent to a third-party frame.
  • Sandboxed frames: a sandbox attribute can change script and origin behavior; capture still depends on whether the browser paints the content.

Reliable capture checklist

  • Use a deterministic URL and a fixed browser version where possible.
  • Set viewport, device scale factor, color scheme, locale, and timezone explicitly.
  • Wait for a frame selector that represents usable content.
  • Verify the iframe locator resolves to the intended frame when several frames exist.
  • Check the bounding box and fail clearly when it is null or zero-sized.
  • Capture after the final scroll position and hide transient overlays.
  • Save diagnostics such as the page URL, frame URL, viewport, and timeout reason.
  • Retry transient navigation failures with a bounded retry count; do not retry permanent framing or authorization errors indefinitely.

Troubleshooting

Symptom Likely cause Fix
contentDocument is null or inaccessible Cross-origin policy Do not read the frame DOM from the parent. Capture rendered pixels with Playwright or CDP, or use an intentional postMessage integration.
Iframe screenshot is blank Frame has not rendered, is hidden, or was blocked Wait for an inside-frame readiness selector, verify dimensions, inspect response and console errors, and check framing headers.
Timeout waiting for a frame locator Wrong selector, delayed navigation, or conditional frame Inspect the frame tree, use a stable iframe attribute, increase the timeout only after diagnosing the delay, and handle the frame’s optional state.
Screenshot cuts off content Only the visible iframe box was captured Scroll and capture sections, use a page clip, or use the embedded app’s own export if it provides one.
Pixels differ between runs Fonts, animations, viewport, device scale, or remote data changed Pin environment settings, wait for stable content, disable animations, and compare with a tolerance.
Capture permission error Screen Capture API lacks user interaction, permission, or secure context Use a secure context and explicit user action, or switch to browser automation for unattended screenshots.
Frame never appears X-Frame-Options or CSP frame-ancestors blocks embedding Change the embedding policy if you control the service, use an approved integration, or capture the source page directly.
Removing transient overlays before capture keeps the iframe screenshot usable.
Removing transient overlays before capture keeps the iframe screenshot usable.

Performance, reliability, and cost

Browser startup is usually the expensive part of small jobs. Reuse a browser process for batches, reuse contexts when isolation permits, and avoid waiting for global network idle when one frame-ready selector is sufficient. Parallelize only within the CPU, memory, and network limits of your runner; too many Chromium pages can increase timeouts and make rendering less stable.

For reliable output, treat remote iframe data as changing input. Add bounded timeouts, capture logs on failure, and keep a screenshot of the parent page when diagnosing layout problems. Cache deterministic results when the embedded content has an acceptable freshness window. A manual or self-hosted workflow has infrastructure and browser-maintenance costs even when the software itself is free.

Or skip the browser setup

ScreenshotNeo provides a website screenshot API and MCP server. It can capture a URL that contains an embedded iframe without you managing Playwright or Chromium. Cookie and consent banners are accepted and removed before capture, along with 60+ known consent platforms, newsletter popups, and chat widgets. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing status.

See the ScreenshotNeo API documentation for all options. The basic request returns PNG, JPEG, WebP, or PDF according to the parameters you choose:

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
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)
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 failed: ${res.status}`);
const data = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', data));

You can also select one element by CSS selector, wait for a selector or delay, load lazy images, set viewport and device presets, use custom headers, cookies, user agents, timezone or geolocation, block requests or resource types, add custom CSS or JavaScript, hide selectors, and choose caching TTL. The MCP server exposes take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. Plans include 1,000 screenshots per month free with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

FAQ

Can I screenshot a cross-origin iframe?

Yes, if the browser successfully renders it. Capture the iframe element or page region with browser automation; do not try to read its DOM from the parent script.

Should I capture the iframe or the page?

Capture the iframe element when you need only its visible rectangle. Capture a page clip when parent-page overlays, surrounding layout, or precise coordinates matter.

Why does CORS not solve iframe DOM access?

CORS controls network sharing, while same-origin policy controls script access to another document’s DOM. They are separate mechanisms.

Is getDisplayMedia() suitable for server-side screenshots?

No. It is designed for user-approved screen or window capture and returns a live stream. Use Playwright, CDP, or a screenshot API for unattended jobs.

How do I capture an iframe that refuses to load?

Inspect framing headers and browser console errors. If the service blocks your origin with CSP or X-Frame-Options, you need an approved integration or must capture the source page directly.