ScreenshotNeo

BlogHow-to

Playwright Screenshot Is Blank After Clicking a Link: Fix Navigation Waits

A click can finish before the destination is ready to capture. Choose the right wait for document navigation, SPA updates, and visual snapshots.

By the ScreenshotNeo team4 October 20268 min read

A Playwright screenshot taken after clicking a link can be blank when the click has finished but the page has not yet rendered the content you intend to capture. Match the wait to the behavior: use page.waitForURL() for a known document destination, and a retrying locator assertion for an SPA route or in-page update. Then capture the page.

Before changing waits, establish the expected outcome. A click may trigger a full document navigation, change content through client-side routing, reveal content without changing the URL, or open a new tab. Those cases need different signals. A URL wait is not proof that an application has rendered its destination content.

  1. Record the current URL before the click and inspect it after the click.
  2. Decide whether the expected result is a new document, a route change, a visible content update, or a new page.
  3. Choose a destination-specific condition that proves the content you want in the screenshot is present.

Playwright waits for a click target to be actionable, and when a click triggers navigation it waits for navigation to happen and the page to start loading. Those steps do not guarantee that the application has rendered the specific content for your capture. A poorly hydrated page can also make an action appear to do nothing; treat that as a possibility to investigate, not a diagnosis from the screenshot alone. [Playwright navigation guide] [Page API] [Actionability]

2. Fix a known document navigation

Wait for the expected main-frame URL, then assert a meaningful destination element before capturing. This is a complete Playwright Test example in TypeScript:

import { test, expect } from '@playwright/test';

test('captures the annual report after navigation', async ({ page }) => {
  await page.goto('https://example.com/reports');

  await page.getByRole('link', { name: 'Open report' }).click();
  await page.waitForURL('**/reports/annual');
  await expect(
    page.getByRole('heading', { name: 'Annual report' })
  ).toBeVisible();

  await page.screenshot({ path: 'report.png', fullPage: true });
});

Replace the example URL, link name, path, and heading with values from your application. A destination-specific assertion catches cases where the URL changed but the useful page content has not appeared.

If you need to register the URL wait before clicking—for example, because the navigation may happen quickly—start both operations together:

await Promise.all([
  page.waitForURL('**/reports/annual'),
  page.getByRole('link', { name: 'Open report' }).click(),
]);
await expect(
  page.getByRole('heading', { name: 'Annual report' })
).toBeVisible();
await page.screenshot({ path: 'report.png' });

page.waitForURL() waits for the main-frame URL to match. You can use a glob such as '**/reports/annual', a regular expression, or a predicate when you need to match more precisely. Set a deliberate timeout if the application has a known slower response, but keep the destination assertion: a longer URL timeout alone does not establish readiness. See the Page API for waitForURL.

Avoid page.waitForNavigation() for new code. The Playwright Page API marks it deprecated and says, “This method is inherently racy, please use page.waitForURL() instead.” [Page API: waitForNavigation]

3. Fix an SPA route or in-page content update

When the app renders through client-side routing, or the URL does not change, wait for the expected application state rather than a document navigation:

import { test, expect } from '@playwright/test';

test('captures the report after the app updates', async ({ page }) => {
  await page.goto('https://example.com/reports');

  await page.getByRole('link', { name: 'Open report' }).click();
  await expect(
    page.getByRole('heading', { name: 'Annual report' })
  ).toBeVisible();

  await page.screenshot({ path: 'report.png', fullPage: true });
});

Locator assertions retry while checking their condition, so the assertion can wait for the heading to become visible. Choose an element that signifies the actual content needed in the screenshot—a report heading, loaded table row, or confirmation—not merely a generic application shell. See Playwright web-first assertions.

A click’s actionability checks establish that its target is unique, visible, stable, able to receive events, and enabled. They do not assert that a later content update has appeared. [Playwright actionability]

4. Capture a stable visual snapshot

For a visual regression test using Playwright Test, use toHaveScreenshot() after waiting for the intended post-click state:

import { test, expect } from '@playwright/test';

test('matches the annual report screenshot', async ({ page }) => {
  await page.goto('https://example.com/reports');
  await page.getByRole('link', { name: 'Open report' }).click();
  await expect(
    page.getByRole('heading', { name: 'Annual report' })
  ).toBeVisible();

  await expect(page).toHaveScreenshot('annual-report.png', {
    fullPage: true,
  });
});

The screenshot assertion waits for two consecutive screenshots to match before comparing the result with the expectation. It is specific to the Playwright test runner; it does not replace choosing the right post-click readiness condition. A stable blank page can also produce a stable image, so first assert the destination state. [Visual comparisons]

5. Handle other click outcomes

If the expected destination is a separate page, wait for the new page and capture that page, rather than the original one. For example, with Playwright Test’s browser context fixture:

const newPagePromise = page.context().waitForEvent('page');
await page.getByRole('link', { name: 'Open report' }).click();
const reportPage = await newPagePromise;

await expect(
  reportPage.getByRole('heading', { name: 'Annual report' })
).toBeVisible();
await reportPage.screenshot({ path: 'report.png' });

For a popup tied directly to a click, Playwright also provides the page’s popup event. Confirm which page received the expected URL and content before capture. See Playwright pages.

The click should reveal content without navigation

Wait for the revealed panel, dialog, or result to become visible. If it is hidden until the click, assert its visible state after the click. Do not wait for a URL change that is not part of the interaction.

The click appears to do nothing

Check that the locator resolves to the intended link and that the application is hydrated and able to handle the event. Playwright’s navigation guide describes poor page hydration as a likely explanation in a scenario where an action occurs but nothing seemingly happens. It is one diagnostic possibility, not a universal explanation. [Navigation guide]

6. Avoid generic waits that hide the real condition

Wait Why it can mislead Better signal
waitUntil: 'networkidle' It means no network connections for at least 500 ms, and Playwright discourages using it for tests. Pages may continue to update after that point, or ongoing requests may prevent it. Assert the destination content or state the screenshot needs.
page.waitForTimeout(2000) A fixed delay can be too short on a slow run and waste time on a fast one. Playwright says to use this only for debugging. Use a URL wait for navigation or a retrying web assertion for rendered content.
page.waitForNavigation() Deprecated and documented as inherently racy. Use page.waitForURL() for the expected main-frame URL.

Playwright’s guidance is to rely on web assertions for readiness in tests. Treating a fixed sleep as proof of rendering can mask a synchronization issue while making the test slower or flaky; that is an inference from the documented warning about timer-based waits. [Page API]

7. Troubleshooting blank screenshots

Symptom Likely check Fix
Screenshot is blank, but the click returned The click’s actionability completed, while destination content may still be rendering. Wait for the destination URL if it is a document navigation, then assert a content-specific locator.
The URL never changes The link may update an SPA, reveal content in place, be blocked, or have failed to trigger. Check expected behavior and assert the resulting content. If navigation should occur, inspect the locator and app event handling.
waitForURL() times out The expected URL pattern may be wrong, the click may not navigate, or the destination may be a new tab. Log or inspect page.url() after the click; verify the pattern and which page is changing.
URL matches, but the screenshot is still empty or incomplete The document reached the URL, but the application content may not be ready or the wrong page may be captured. Assert the specific heading, result, or other content required; for a new tab, capture the new page.
Assertion times out on the destination heading The locator may not match, content may have failed to load, or the click may have led somewhere else. Inspect the rendered page, confirm accessible name and role, and check the current URL and application error state.
Test passes but visual output is consistently blank A screenshot stability assertion can stabilize the wrong state. Assert the intended destination content before toHaveScreenshot().

The available information cannot identify the cause of a particular blank capture without the app, Playwright version, browser, screenshot, and link behavior. Start with the observed URL and the expected page state; those distinguish a wait problem from a click or destination problem.

8. Performance, reliability, and cost

A URL wait followed by a targeted assertion avoids an arbitrary delay and ties completion to the outcome that matters. Keep waits scoped to the expected URL and content, and use reasonable timeouts for your application. A generous fixed sleep slows every fast run and can still fail a slower one; a generic network-idle condition can be unreliable when a page makes continuing requests. These are practical consequences of the documented wait semantics, not measured benchmark claims.

For repeated visual comparisons, let Playwright Test’s screenshot assertion handle consecutive-image stability after the page state is correct. It reduces noise from transient visual changes, but cannot correct a mistaken readiness condition. Playwright and the dossier provide no cost or performance benchmark for this specific fix; execution cost depends on your test environment and run volume.

9. Or skip the browser setup

If you need a website image without maintaining browser automation and navigation waits, ScreenshotNeo is a website screenshot API and MCP server for developers. Its one-call API captures a URL as PNG, JPEG, WebP, or PDF. See the ScreenshotNeo API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo accepts cookie and consent banners 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 cost nothing, 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 each month with no card. Paid plans start at $5 for 3,000 screenshots; all features are available on every plan. Sign up for 1,000 free screenshots a month, no card required.

10. FAQ

Does a successful click mean the page is ready for a screenshot?

No. It means the click met its actionability checks and any click-triggered navigation reached the point Playwright waits for. Assert the content you intend to capture.

Should I wait for the URL or for a heading?

For a known document destination, wait for the URL and then assert a meaningful heading or other destination content. For an SPA or in-page update, the content assertion is usually the useful readiness signal.

Can toHaveScreenshot() fix a blank capture?

It waits for consecutive screenshots to match in Playwright Test, but it does not determine whether the page is the intended destination. Assert the post-click state first.

Why does networkidle sometimes hang or still capture too early?

It tracks network connections, not whether the specific application content you need is visible. Ongoing requests can delay it, and a quiet network does not prove the desired state rendered.