What to Do if a Lazy-Loaded Iframe Appears Blank in a Playwright Screenshot
Scroll the iframe into view, wait for meaningful content inside it, then capture. Learn how to diagnose blank frames and avoid flaky screenshot waits.
If a lazy-loaded iframe appears blank in a Playwright screenshot, first bring the iframe into view, then wait for a meaningful element inside the frame to appear, and only then take the screenshot. The iframe box being visible, a page load event, or networkidle does not prove that the embedded application has rendered its UI. Use a locator assertion for the content you expect.
This sequence separates three different conditions: the iframe element exists, its document has loaded, and its intended interface is ready. Playwright provides frame-scoped locators and retrying assertions for checking the last condition. See the official FrameLocator API and locator documentation.
1. Scroll the iframe into view and assert on its content
Here is a complete JavaScript example using Playwright Test. Replace the page URL, iframe selector, and expected heading with values from your page. The assertion is the readiness check: it retries until the heading is visible or the test timeout is reached.
import { test, expect } from '@playwright/test';
test('captures the rendered report iframe', async ({ page }) => {
await page.goto('https://example.com/dashboard');
const iframe = page.locator('iframe#report');
const report = page.frameLocator('iframe#report');
// Trigger viewport-based lazy loading when the page uses it.
await iframe.scrollIntoViewIfNeeded();
// Wait for application content, not just the iframe element or document.
await expect(
report.getByRole('heading', { name: 'Report' })
).toBeVisible();
await page.screenshot({ path: 'report.png' });
});
Install Playwright Test in the project with npm init playwright@latest if it is not already present, then run the test with npx playwright test. Check your installed Playwright version if an API shown here is unavailable; the official documentation describes the current API.
Use a stable selector that identifies the intended iframe. A FrameLocator is strict: if its selector matches more than one frame, an operation fails. If a page has several embeds, narrow the selector using a stable ID, name, or container relationship rather than selecting the first arbitrary iframe. See FrameLocator strictness and the guide to working with frames.
2. Choose a readiness signal that matches the embedded UI
The outer iframe and the content inside it have separate readiness conditions. Scrolling may trigger a page’s lazy-load behavior, but that trigger depends on how the page is implemented. After scrolling, assert on an inner element that means the embed is usable: a heading, status label, chart container, or a test-specific ready marker.
Prefer a web-first assertion
Locator assertions such as toBeVisible() retry while the condition is false. This makes them a better fit for UI readiness than a fixed sleep. For example, if the embed has no heading, assert against a stable status element instead:
await expect(
report.locator('[data-testid="report-ready"]')
).toBeVisible();
The selector must describe content that actually indicates readiness. A generic iframe body or a decorative wrapper may exist before the useful content does. Playwright explains locator retry behavior in its locators guide.
Use load-state waits only when you need that lifecycle state
frame.waitForLoadState() waits for a frame document lifecycle state; it does not assert that an application-specific widget has finished rendering. Playwright notes that actions usually auto-wait and that networkidle is discouraged as a test readiness signal. Prefer an assertion on the intended UI. See the Frame API.
If you have a concrete reason to wait for a frame lifecycle event, you can access the frame and wait for its load state, then still assert on the content:
const frame = page.frame({ name: 'report-frame' });
if (!frame) throw new Error('Report iframe was not found');
await frame.waitForLoadState('domcontentloaded');
await expect(
page.frameLocator('iframe#report').getByRole('heading', { name: 'Report' })
).toBeVisible();
Frame selection by name only works when the iframe has the matching name. If the frame is anonymous or names are not stable, use a locator for the iframe and its FrameLocator for content. The load-state wait is optional; the inner assertion is what verifies the expected UI.
3. Decide what to capture
Once the expected content is visible, choose the screenshot scope that matches the debugging or test goal.
- Viewport:
await page.screenshot({ path: 'page.png' })captures the current viewport. Ensure the relevant part of the page is visible if the screenshot is meant to show it. - Full page:
await page.screenshot({ path: 'page-full.png', fullPage: true })captures the full scrollable page. A full-page screenshot option does not, by itself, prove a lazy embed was triggered or rendered; perform the scroll and content assertion first. - Iframe element:
await page.locator('iframe#report').screenshot({ path: 'iframe.png' })captures the iframe element. This targets the frame box, so first verify the frame’s inner content is ready. - An element inside the frame:
await report.getByRole('heading', { name: 'Report' }).screenshot({ path: 'heading.png' })captures a specific inner element. Element screenshot actions scroll the target into view and perform actionability checks; see the ElementHandle screenshot documentation. You can use a locator screenshot for a more useful target than the heading itself, such as a chart container.
For an element screenshot, use a locator for a stable target inside the iframe. Keep the content assertion even if the screenshot action scrolls the target: the assertion documents the expected ready state and produces a clearer failure when the embed never renders.
4. Diagnose a frame that remains blank
If the inner assertion times out, treat that as a diagnostic result rather than adding a longer arbitrary delay. Inspect whether the intended frame exists, whether the expected inner selector matches the actual page, and whether the browser reports console or network errors. These checks help locate the failure layer; the evidence available for this issue cannot identify a particular third-party embed cause in advance.
- Confirm the iframe selector. Check that
iframe#reportmatches the intended frame and only that frame. Tighten the selector if several embeds match. - Confirm the lazy-load trigger. Scroll the iframe itself into view. If the page uses another trigger, such as an interaction or a parent container becoming visible, reproduce the behavior the page expects.
- Confirm the inner readiness selector. Inspect the embedded page and choose a meaningful element that exists when its UI is ready. A changed heading, status message, or test marker may be more reliable than a generic wrapper.
- Inspect browser diagnostics. Record console errors and failed requests around the capture, then inspect the iframe URL and the failing request. This can reveal an application or network problem, but do not infer a cause without the logs.
- Check frame boundaries and access patterns. Use
frameLocatorto scope locators to iframe content. Follow the official frames guide if the frame structure is nested or the selector strategy is unclear.
5. Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| The iframe box is visible but the screenshot is blank. | The embedded UI has not rendered, even though the outer element is present. | Scroll the iframe into view and assert that a meaningful inner element is visible before capturing. |
| The frame locator operation reports a strict-mode violation. | The selector matched more than one iframe. | Use a narrower stable selector for the intended frame. |
| The heading assertion times out. | The content selector may be wrong, the lazy-load trigger may not have fired, or the frame may have failed to render. | Verify the selector and trigger, then inspect the iframe URL, console errors, and failed requests. |
waitForLoadState('networkidle') completes but the frame is still blank. |
Network idleness is not a guarantee that application UI is ready; background requests can also make this signal unsuitable. | Wait for the expected inner UI with a web-first assertion. |
| A fixed delay sometimes works and sometimes fails. | Rendering time varies, so a time-only condition is flaky. | Replace the delay with a retrying assertion on the actual ready content. Playwright describes timeout waits as debugging tools, not a normal synchronization strategy, in its Frame API. |
| The element screenshot does not include the whole page. | An element screenshot captures its target, not the entire scrollable document. | Use page.screenshot({ fullPage: true }) for the full page, after triggering and verifying the iframe. |
6. Reliability, speed, and maintenance
- Reliability: Assert on the smallest stable element that proves the embed is ready. That gives a specific failure when the content is absent and avoids confusing document load with UI readiness.
- Speed: A retrying assertion proceeds as soon as its condition passes, rather than always waiting out a fixed delay. Avoid adding multiple lifecycle waits unless the test needs them.
- Maintenance: Prefer stable IDs, accessible roles and names, or deliberate test IDs over brittle positional selectors. When a page changes, update the readiness condition to match the intended UI.
- Version context: Playwright APIs evolve. If a method or option is missing in an older installation, check the documentation and release notes for the project’s installed version: Playwright release notes.
- Cost: Playwright is the browser automation method shown here; infrastructure and browser-run costs depend on where and how the project runs it. No cost or performance benchmark is implied by this guide.
7. Or skip the browser setup
If you need a screenshot without setting up a browser flow, ScreenshotNeo is a website screenshot API and MCP server. It returns a PNG, JPEG, WebP, or PDF from one GET request. For this example, replace YOUR_API_KEY with your key and change the target URL as needed.
cURL:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python:
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)
Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
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(fs => fs.writeFile('shot.webp', image));
See the ScreenshotNeo API documentation for request options. ScreenshotNeo accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots. Every feature is on every plan.
Sign up free for 1,000 screenshots a month; no card required.
8. FAQ
Does frameLocator work with cross-origin iframes?
Playwright’s frame APIs are designed to work with frames. Use the frame locator to scope selectors to the embedded content, and consult the official frames guide for the structure on your page.
Should I use a fixed timeout after scrolling?
Use a fixed timeout only as a short debugging aid. For a repeatable screenshot, wait for the expected inner content with a locator assertion.
Will fullPage: true trigger every lazy-loaded iframe?
Do not rely on full-page capture as the lazy-load trigger. Scroll the target iframe into view and verify its content before taking the full-page screenshot.
When was frameLocator introduced?
Playwright’s release notes list the frame locator introduction in version 1.17. Check the project’s installed version and its matching documentation if the API is unavailable: release notes.


