ScreenshotNeo

BlogHow-to

How to Capture a Webpage Screenshot After Waiting for a Selector in Playwright

Wait for the right Playwright locator state, then capture a viewport, full page, or element. Includes runnable JavaScript, Python, cURL, and Node.js examples.

By the ScreenshotNeo team4 October 20268 min read

To capture a webpage screenshot after a selector is ready, create a Playwright locator, wait for the state you need, and then call the screenshot method. In Node.js, the core pattern is:

const target = page.locator('.ready');
await target.waitFor({ state: 'visible' });
await page.screenshot({ path: 'page.png' });

Replace .ready with a selector for the content that should be ready in the image. Use visible when it must appear on screen. Use attached when presence in the DOM is enough, or hidden/detached when you are waiting for an element to disappear. A timeout means the chosen readiness condition was not met; do not treat it as a reason to capture a potentially incomplete page. See the official Locator API and screenshot API.

1. Set up a runnable Playwright script

This complete Node.js example opens a page, waits for a visible selector, and saves a viewport screenshot. Install Playwright and its browser first:

npm init -y
npm install playwright
npx playwright install chromium

Save as screenshot.mjs and run node screenshot.mjs:

import { chromium } from 'playwright';

const browser = await chromium.launch({ headless: true });
try {
  const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
  await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });

  const target = page.locator('[data-testid="main-content"]');
  await target.waitFor({ state: 'visible', timeout: 10_000 });
  await page.screenshot({ path: 'page.png' });
} finally {
  await browser.close();
}

Change the URL and selector to match your page. The try/finally ensures the browser is closed even if navigation, waiting, or capture fails. Locator waiting is preferred to the older page.waitForSelector() API, which Playwright marks as discouraged; see the Page API guidance.

2. Choose the right wait state

A selector wait answers a specific question about one element. Pick the state that matches what the screenshot needs:

State Waits until Use when
visible The element is in the DOM and has a visible, non-empty bounding box without visibility:hidden. The element itself should appear in the screenshot.
attached The element exists in the DOM. DOM insertion is the readiness signal, or visibility is irrelevant.
hidden The element is absent, or present but not visible. A loading overlay or blocking panel must go away.
detached The element is no longer in the DOM. The page removes a spinner or temporary node before capture.

Example: wait for a loading indicator to disappear and then wait for the content to appear:

await page.locator('.loading').waitFor({ state: 'hidden', timeout: 10_000 });
await page.getByRole('main').waitFor({ state: 'visible', timeout: 10_000 });
await page.screenshot({ path: 'page.png' });

A visible target does not prove that every other part of the page has finished loading. Images, fonts, animations, and unrelated async content may still change. When the page exposes a meaningful readiness marker, wait for it too. Avoid replacing a real readiness condition with a fixed sleep: a delay can be too short on a slow run and waste time on a fast one.

3. Pick a locator that survives page changes

Prefer locators based on accessible, user-facing properties or explicit test IDs. They are easier to understand and less likely to break than selectors tied to a long chain of implementation details. Playwright documents locator recommendations in its locators guide.

// Accessible role and name
const main = page.getByRole('main');

// Text, label, placeholder, or test ID
const heading = page.getByRole('heading', { name: 'Monthly report' });
const search = page.getByPlaceholder('Search');
const panel = page.getByTestId('report-panel');

await panel.waitFor({ state: 'visible' });
await page.screenshot({ path: 'report.png' });

CSS selectors remain useful when the page has a stable class, ID, or data attribute:

await page.locator('#report-ready').waitFor({ state: 'visible' });

Make sure the locator identifies one intended element. If the page can contain multiple matches, scope the locator to a parent or use a more specific accessible name or test ID.

4. Choose screenshot scope and output

Playwright captures the current viewport by default. Use the method that matches the artifact you need:

Need Code Result
Viewport await page.screenshot({ path: 'page.png' }) Visible viewport area.
Full scrollable page await page.screenshot({ path: 'page.png', fullPage: true }) Full page, beyond the current viewport.
One element await target.screenshot({ path: 'element.png' }) The locator’s element.
In memory const buffer = await page.screenshot() Image bytes for upload or further processing.

Full-page capture can expose content below the fold and may take longer or produce a much taller image. Element capture is useful for a component artifact, but it does not capture the rest of the page. The official screenshot guide shows the documented capture modes.

5. Python example

Playwright for Python uses the same wait-then-capture workflow. Install the package and browser:

python -m pip install playwright
playwright install chromium

Save as screenshot.py and run with python screenshot.py:

from playwright.sync_api import sync_playwright

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

        target = page.locator('[data-testid="main-content"]')
        target.wait_for(state="visible", timeout=10_000)
        page.screenshot(path="page.png")
    finally:
        browser.close()

For an element image, call target.screenshot(path="element.png"). For a full-page file, pass full_page=True to page.screenshot().

6. cURL and Node.js alternatives

cURL does not run Playwright or control a browser, so it cannot wait for a DOM selector by itself. It can call a screenshot service that performs browser capture on the server. Node.js can use Playwright directly as shown above, or make the same service request with fetch.

For a direct cURL request to ScreenshotNeo, replace the example target URL as needed. See the ScreenshotNeo API documentation for request parameters and response details:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

The equivalent Node.js service request is:

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

This service call captures a URL; it does not provide the local Playwright selector-wait step. Use Playwright when your workflow must wait on a particular DOM selector before capture.

7. Use screenshot assertions for visual tests

Saving an image and asserting that a page matches a baseline are different jobs. In Playwright Test, toHaveScreenshot() waits for consecutive screenshots to stabilize and compares the final capture with the expected snapshot. Use it for visual regression tests, not as a replacement for a normal image artifact:

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

test('report renders', async ({ page }) => {
  await page.goto('https://example.com');
  await page.getByTestId('report-panel').waitFor({ state: 'visible' });
  await expect(page).toHaveScreenshot('report.png');
});

Install and run the Playwright Test runner using its snapshot assertion documentation. Screenshot rendering can differ with the operating system, browser version, settings, hardware, power source, and headless mode. Keep the environment consistent when reviewing visual diffs; see Playwright’s visual comparison notes.

8. Troubleshooting

Symptom Likely cause Fix
Timeout waiting for selector The selector does not match, the page did not reach the expected state, or navigation failed. Check the URL and selector, inspect the page, and choose a condition tied to real readiness. Increase the timeout only if the page legitimately needs longer.
Element is attached but screenshot looks incomplete DOM presence was mistaken for visual or page readiness. Wait for visible and for any separate page-specific readiness marker needed by the image.
Screenshot is only a small viewport page.screenshot() defaults to the viewport. Set fullPage: true or use the target locator’s screenshot method.
Wrong or multiple elements matched The selector is too broad or coupled to changing page markup. Use a role, accessible name, test ID, or a locator scoped to a stable parent.
Capture runs before async content settles The waited-for selector is unrelated to the late content. Wait for the actual content’s state or another page-specific signal; do not assume one element means every request completed.
Visual test changes across machines Rendering environment differs. Use a consistent browser, OS, and capture configuration for baseline creation and comparison.

9. Performance, reliability, and cost

Keep the wait focused on the condition that matters for the capture. Fixed delays add latency to every run, while overly broad readiness conditions can wait on resources unrelated to the screenshot. Full-page images and large element captures can take more time and memory than viewport captures. Set a bounded timeout and handle its failure explicitly so automation reports a readiness problem rather than silently saving a misleading image.

Local Playwright costs include the compute and browser environment you operate; this workflow has no per-screenshot API charge described here. A hosted screenshot API trades local browser setup for a request-based service and its plan limits. ScreenshotNeo offers 1,000 screenshots/month free with no card; paid plans are Starter $5 for 3,000, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000. Yearly billing gives two months free. Every feature is on every plan. Clean shots are billed; bot checks/CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not, with verdict and billing details in response headers.

10. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server for developers, made by Yorker Media. One GET request can return a screenshot or PDF; learn about ScreenshotNeo and see the API documentation.

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)

Cookie banners are accepted and removed before the shot, along with known newsletter popups and chat widgets; each step can be turned off. Bot checks, blank pages, timeouts, and failed loads are never billed. An MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month, no card required.

11. FAQ

Does waiting for a selector guarantee all images have loaded?

No. It guarantees only the requested locator state. Add a page-specific readiness condition for content that matters but is independent of the target.

Should I use a screenshot assertion to save an image?

Use page.screenshot() for an image artifact. Use toHaveScreenshot() when a Playwright Test should compare the page against a visual baseline.

Can cURL wait for a selector in my local page?

No. cURL does not control a browser DOM. It can call a hosted screenshot API, while Playwright performs selector-based waits in a browser session.