ScreenshotNeo

BlogHow-to

Fix White Screenshots of Websites That Render Inside an iframe

A white iframe screenshot usually means the frame was not ready, never loaded, or fell outside the capture. Diagnose the cause and wait for visible content before capturing.

By the ScreenshotNeo team4 October 20269 min read

A white screenshot of a website that renders content inside an iframe usually means the capture happened before the embedded content was visible, the frame was never triggered to load, the content failed to load, or the screenshot did not include the frame. Make the capture wait for a meaningful signal inside the intended frame, after bringing lazy-loaded frames into view. A page or iframe load event alone does not prove the embedded content rendered.

This guide uses Playwright for the main runnable example and includes Puppeteer, cURL, Python, and Node.js options. The same diagnosis applies whether you capture a viewport, an element, or the full page.

1. Confirm which frame should appear in the screenshot

Start by finding the iframe and checking its src, dimensions, and position. A screenshot can look blank because the frame is genuinely empty, because content has not painted yet, or because the frame is outside the viewport or screenshot crop.

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

For nested iframes, inspect each frame’s URL and identify the one containing the expected application or page. If the URL is empty or unexpected, investigate how the embed URL is generated before changing screenshot timing.

2. Trigger lazy iframe loading

An iframe with loading="lazy" may not be requested until it is near the visual viewport. Scroll the target into view, then wait for the frame and its content. A full-page screenshot should not be assumed to trigger every page’s lazy-loading behavior. [MDN: iframe loading and events]

const iframe = page.locator('iframe#report');
await iframe.scrollIntoViewIfNeeded();

If the iframe is inside a collapsed tab or accordion, make that section visible first. A hidden frame may not load or render as expected until its container is opened.

3. Wait for visible content inside the frame

Wait for a stable signal that belongs to the embedded application, such as its heading, root element, or the disappearance of its loading indicator. Choose a selector that means the content you need is ready; there is no universal selector or fixed delay that works for every site.

Playwright can locate a frame by its iframe element and then wait for a locator inside it. Set a bounded timeout so a blocked or broken embed produces a useful error instead of hanging indefinitely.

const frame = page.frameLocator('iframe#report');
await frame.getByRole('heading', { name: 'Monthly report' }).waitFor({
  state: 'visible',
  timeout: 15000,
});
await page.locator('iframe#report').screenshot({ path: 'iframe.png' });

Replace the selector and heading with signals from the target site. If the app renders a loading indicator, waiting for it to disappear can be useful, but pair that with a visible-content check where possible. Navigation reaching domcontentloaded or load only describes the document lifecycle; it does not establish that a client-rendered app has finished painting.

4. Runnable Playwright example

This Node.js script navigates to a page, finds a named iframe, brings it into view, waits for a heading in the embedded content, and captures the iframe element. It reports frame URLs if the expected content does not appear.

import { chromium } from 'playwright';

const targetUrl = 'https://example.com/page-with-iframe';
const iframeSelector = 'iframe#report';
const expectedHeading = 'Monthly report';

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

try {
  await page.goto(targetUrl, {
    waitUntil: 'domcontentloaded',
    timeout: 30000,
  });

  const iframeElement = page.locator(iframeSelector);
  await iframeElement.waitFor({ state: 'attached', timeout: 15000 });
  await iframeElement.scrollIntoViewIfNeeded();

  const frame = page.frameLocator(iframeSelector);
  await frame.getByRole('heading', { name: expectedHeading }).waitFor({
    state: 'visible',
    timeout: 20000,
  });

  await iframeElement.screenshot({ path: 'iframe.png' });
  console.log('Saved iframe.png');
} catch (error) {
  console.error('Iframe capture failed:', error.message);
  console.error('Observed frames:', page.frames().map((frame) => frame.url()));
  throw error;
} finally {
  await browser.close();
}

Install Playwright with npm install playwright and install its browser with npx playwright install chromium. For a viewport screenshot, use await page.screenshot({ path: 'page.png' }); for a full-page screenshot, use await page.screenshot({ path: 'page.png', fullPage: true }). For an element inside the iframe, locate it with frame.locator('css-selector') and screenshot that locator. Playwright documents page navigation waits, frame locators, and screenshot options in its Page API and frames guide.

5. Puppeteer alternative

Puppeteer supports frame inspection and element screenshots as well. This example waits for the expected heading through the matching frame before capturing the iframe element.

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();

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

  const iframeElement = await page.waitForSelector('iframe#report', {
    timeout: 15000,
  });
  await iframeElement.evaluate((element) => element.scrollIntoView({ block: 'center' }));

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

  await frame.waitForSelector('h1', { visible: true, timeout: 20000 });
  await iframeElement.screenshot({ path: 'iframe.png' });
} catch (error) {
  console.error('Iframe capture failed:', error.message);
  console.error('Observed frames:', page.frames().map((frame) => frame.url()));
  throw error;
} finally {
  await browser.close();
}

Change the URL fragment used to identify the frame and the selector to match the target. Puppeteer’s screenshot guide shows navigation waits and both page and element screenshot methods: Puppeteer screenshots.

6. Check whether the embed failed

Waiting longer cannot fix a frame that the server refuses to embed, an expired authentication session, a missing resource, or a request blocked by the browser or network. Inspect browser console messages and failed network requests, then check the iframe URL directly if access and authentication permit.

An iframe’s load event can fire even when its content failed to load, while lazy-loaded frames do not affect the parent page’s load-event timing. Browsers also do not expose an iframe error event as a dependable failure signal. Treat these events as lifecycle clues, not proof that the expected content is visible. [MDN iframe reference]

7. Account for cross-origin restrictions

If the iframe uses another origin, page JavaScript cannot freely read its DOM because of the same-origin policy. Browser automation can select a frame and interact with its accessible content, but it cannot make a blocked embed succeed. If you control both applications, an intentional postMessage handshake can signal readiness. Otherwise, rely on automation frame APIs and browser diagnostics. Do not disable browser security as a routine screenshot fix. [MDN: same-origin policy]

8. Capture the intended region and output

  • Viewport: Captures what is currently visible. Scroll the iframe into the intended viewport region before capture.
  • Iframe element: Captures the frame’s rectangular element bounds, which is useful when only the embed is needed.
  • Inner element: Locate the content through the frame API and capture that element when the outer iframe includes padding or surrounding controls.
  • Full page: Captures the page extent, but does not guarantee that all application-specific lazy content has loaded.
  • Scale and size: Set viewport dimensions and device scale deliberately. Confirm the frame is not clipped and that responsive layout has not hidden the content at the chosen width.

Playwright also supports screenshot styling that can apply into inner frames. This can help standardize appearance, but does not resolve a failed request or authentication requirement. See Playwright screenshot options.

9. cURL, Python, and Node.js with ScreenshotNeo

If you need a screenshot without managing a browser process, ScreenshotNeo is a website screenshot API and MCP server for developers. One GET request accepts a URL and returns an image or PDF. It captures the rendered page, so an iframe still needs to load successfully and be visible in the target page. The following request captures a target page; replace the URL with the page containing your iframe. 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://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 request failed: ${res.status}`);
const image = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(({ writeFile }) => writeFile('shot.webp', image));

ScreenshotNeo provides options for waiting on a selector, a delay, or network idle, plus full-page capture and custom JavaScript. These can help when the page exposes a suitable readiness condition; they cannot make a server permit embedding or satisfy an unavailable login. Its response includes page-verdict and billing headers, and bot checks, blank pages, failed loads, timeouts, and cache hits are not billed. The product also offers an MCP server with take_screenshot, get_page_info, and capture_pdf tools for AI agents. Plans include 1,000 screenshots per month free without a card; paid plans start at $5 for 3,000 screenshots.

Or skip the browser setup

Use one API call to capture the page containing the iframe:

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

ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. See the API docs and ScreenshotNeo. Create a free account and get 1,000 screenshots a month with no card.

Troubleshooting

Symptom Likely cause What to do
White rectangle, but parent page is visible Capture occurred before embedded app content appeared, or the request failed. Wait for a meaningful locator inside the frame; inspect the frame URL, console, and network failures.
Frame URL is empty or points somewhere unexpected The iframe source is assigned later, built by client code, or not set. Wait for the expected iframe attribute or app state, then inspect frame URLs again.
Iframe never appears in the frame list Lazy loading has not been triggered, the iframe is conditional, or the selector is wrong. Verify the selector, reveal its container, scroll it into view, and wait for attachment.
Timeout waiting for a frame locator The selector is wrong, content differs from the expected state, or the frame is blocked. Log all frame URLs, inspect diagnostics, and choose a selector present in the actual successful state.
Works locally, fails in automation Different viewport, missing cookies or authentication, geolocation, or network policy. Match the viewport and required session state; inspect the request made in the automation browser.
Content appears but screenshot is still blank Capture crop, clipping, stacking, or timing of paint/animation is wrong. Capture the iframe element, verify its bounding box and visibility, and wait for a stable application state.
Parent-page script cannot inspect frame contents Cross-origin browser security boundary. Use automation frame APIs, diagnostics, or an agreed postMessage signal if you control the embed.

Performance, reliability, and cost

Prefer a readiness condition over a long fixed sleep: it can proceed as soon as the required content is visible and fail with a bounded timeout when it never arrives. Network-idle waits are useful for some pages, but long polling, analytics, or persistent connections may prevent an idle state; a frame-specific selector is usually a clearer statement of what the screenshot requires. This is implementation guidance, not a claim of measured speed.

For reliable capture jobs, log the target URL, frame URLs, chosen readiness signal, timeout, and browser console or request failures. Keep authentication material out of logs. Treat a timeout separately from a successful image response, and avoid retrying a permanently blocked embed indefinitely. With ScreenshotNeo, inspect the response’s verdict and billing headers to distinguish clean captures from blank pages, failed loads, bot checks, and cache hits. Its listed pricing is free for 1,000 shots monthly, then $5 for 3,000 on Starter, $15 for 15,000 on Growth, $39 for 60,000 on Pro, $99 for 250,000 on Scale, and $249 for 1,000,000 on Business; yearly billing gives two months free, and every feature is on every plan. See documentation for the available options.

FAQ

Does waitUntil: 'networkidle2' guarantee the iframe is ready?

No. It is one navigation wait strategy. The embedded app may still need to render or may have failed. Wait for a meaningful signal inside the frame.

Can I read every third-party iframe from page JavaScript?

No. The same-origin policy restricts access across origins. Use your automation framework’s frame support or a communication mechanism provided by the embed.

Will waiting longer fix a site that blocks embedding?

No. A longer wait cannot override server-side embed restrictions, missing authentication, or a failed request.

Should I use Playwright or Puppeteer?

Both provide frame and screenshot APIs. Choose based on your existing browser automation stack and the frame, wait, and output controls your workflow needs; the cited documentation does not establish a general reliability winner.