ScreenshotNeo

BlogHow-to

Capture a webpage screenshot in Python with Playwright and wait for a specific selector

Wait for the right element state with Playwright locators, then capture a viewport, full page, or element in Python.

By the ScreenshotNeo team4 October 20268 min read

Use a Playwright locator to identify the element that signals readiness, wait for the state your screenshot needs, then capture the viewport, full page, or that element. For new Python code, prefer locator.wait_for() over the older page.wait_for_selector() API.

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.goto("https://example.com", wait_until="domcontentloaded")

    page.locator("#ready").wait_for(state="visible", timeout=30_000)
    page.screenshot(path="screenshot.png")

    browser.close()

This waits up to 30 seconds for #ready to be visible, then saves the current viewport to screenshot.png. Change the selector, state, timeout, and screenshot scope to fit the page. Playwright locator waiting and screenshot APIs are documented in the Locator API and Screenshots guide.

1. Install Playwright and its browser

Install the Python package and Chromium browser binary in the same environment where the script will run:

python -m pip install playwright
python -m playwright install chromium

Save the synchronous example as capture.py and run it with python capture.py. If you use a virtual environment, activate it before installing and running. Playwright requires browser binaries in addition to the Python package; install the browser you plan to launch. See the official Python installation guide.

2. Wait for the selector state that matches the task

A selector existing in the DOM does not always mean the page is ready to photograph. Choose the state based on what the screenshot needs to show:

State Use it when Meaning
visible The target must appear in the screenshot The element has a non-empty bounding box and is not hidden with visibility:hidden.
attached Presence in the DOM is enough The element is connected to the document, even if not visible.
hidden A loader or overlay must disappear The element is hidden or absent.
detached The element must be removed from the DOM The element is no longer connected to the document.

locator.wait_for() defaults to visible. An element with display:none, an empty bounding box, or no rendered content is not considered visible. Locator waiting has a documented default timeout of 30 seconds. Set a different timeout when the page’s expected load time calls for it; use timeout=0 only when intentionally disabling the limit.

Wait for a result to appear

results = page.locator("#results-loaded")
results.wait_for(state="visible", timeout=10_000)
page.screenshot(path="results.png", full_page=True)

The 10-second timeout is an example, not a universal recommendation. Choose a bound that reflects your application and treat a timeout as a signal to investigate readiness, selector accuracy, or page health.

Wait for a loading indicator to go away

page.locator(".loading-overlay").wait_for(state="hidden", timeout=30_000)
page.screenshot(path="after-loading.png")

Use detached instead of hidden if the page removes the loading element from the DOM and its removal is the condition you care about.

Use a locator that describes the target

CSS is a direct choice when the selector is known, such as page.locator("#ready"). For a user-facing element, a role or label can make the intent clearer:

page.get_by_role("heading", name="Account overview").wait_for(state="visible")
page.get_by_text("Your report is ready").wait_for(state="visible")
page.get_by_label("Email address").wait_for(state="visible")

Other built-in locator choices include placeholder, alt text, title, and test ID. Locators resolve elements when used, which helps when a page re-renders. If a selector can match several elements, narrow it so the intended target is unambiguous.

3. Choose what to capture

Viewport screenshot

page.screenshot() captures the current viewport by default:

page.screenshot(path="viewport.png")

Full scrollable page

Set full_page=True to capture the full page height:

page.screenshot(path="full-page.png", full_page=True)

Very long documents can produce large images and take longer to capture or transfer. Consider whether the consumer needs the whole document or only the visible viewport.

One element

Use the locator’s screenshot method to capture the area occupied by one element:

target = page.locator("main article")
target.wait_for(state="visible")
target.screenshot(path="article.png")

The locator screenshot scrolls the element into view and waits for it to be actionable. It captures the element’s area; if another element covers it, the output may still be obscured. For a scrollable element, the screenshot contains its currently scrolled content rather than automatically expanding all internal content.

Keep the image in memory

Omit path to receive screenshot bytes for processing or passing to another tool:

image_bytes = page.screenshot(full_page=True)
with open("processed-input.png", "wb") as image_file:
    image_file.write(image_bytes)

4. Complete asynchronous version

Use the asynchronous API consistently: await navigation, locator waiting, and screenshot capture.

import asyncio
from playwright.async_api import async_playwright

async def main():
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        page = await browser.new_page()
        await page.goto("https://example.com", wait_until="domcontentloaded")

        target = page.locator("#ready")
        await target.wait_for(state="visible", timeout=30_000)
        await page.screenshot(path="screenshot.png", full_page=True)

        await browser.close()

asyncio.run(main())

For concurrent capture jobs in an async application, create pages from a managed browser and close pages when each job finishes. Avoid launching an unbounded number of browser processes.

5. Navigation and readiness are separate

page.goto() waits according to its navigation condition; a selector wait expresses a page-specific condition. The page can reach a navigation milestone before client-side rendering, data fetching, or a particular widget finishes. Waiting for the meaningful target makes the screenshot condition explicit.

The examples use wait_until="domcontentloaded" to proceed after the initial document has been parsed, then wait for the target. You can also use Playwright’s navigation wait conditions where they fit the site. Network-idle behavior is not a reliable universal readiness test for applications that keep requests open or poll continuously. Prefer the selector that represents the content you actually need.

6. Legacy selector waiting

page.wait_for_selector() remains documented, but Playwright discourages it for new code and recommends locator-based waiting or web-first assertions. Existing scripts can use the older form:

page.wait_for_selector("#ready", state="visible", timeout=30_000)
page.screenshot(path="screenshot.png")

For new code, use page.locator("#ready").wait_for(state="visible"). Avoid replacing a readiness condition with a fixed sleep such as time.sleep(5) or page.wait_for_timeout(5000); fixed delays can be flaky and waste time when the page is ready sooner.

7. Troubleshooting

Symptom Likely cause Fix
Locator wait times out The selector is wrong, the element never reaches the requested state, or the page is slower than the timeout. Inspect the selector and page state; wait for the correct condition and set a realistic bounded timeout.
Wait passes but screenshot is blank or incomplete The selector only proves one element is present; other content may still be loading. Wait for a target that represents the content needed in the image, or wait for a loading indicator to become hidden.
Element is attached but cannot be seen attached means in the DOM, not visible. Use visible when the element must be shown.
Expected element is visible but hidden behind a modal Visibility does not guarantee that another layer is not covering it. Wait for the modal or overlay to become hidden, then capture.
Only part of a long page appears The default page screenshot is viewport-sized. Use full_page=True for the scrollable page.
Element image omits content lower in its scroll area A locator screenshot captures the element’s current rendered area, not all internally scrollable content. Capture the page or scroll the element deliberately before capture, depending on the desired output.
Browser launch reports missing executable The Python package is installed but its browser binary is not. Run python -m playwright install chromium in the active environment.
Sync API used inside an async event loop The synchronous and asynchronous APIs were mixed. Use async_playwright and await each operation, or run the sync flow outside the active event loop.

8. Performance, reliability, and cost

  • Readiness: A precise selector wait avoids arbitrary delays. Keep a finite timeout so a missing target fails visibly instead of leaving a job stuck.
  • Capture scope: Viewport captures usually involve less image area than full-page captures. Full-page output can consume more time, memory, and storage for long documents.
  • Browser reuse: For repeated captures, reuse a browser process and create pages or contexts per job where suitable; close resources after use. This avoids repeatedly paying browser startup cost in time and compute.
  • Determinism: Use a stable selector and a state tied to the intended content. Screenshots can vary with dynamic data, animations, fonts, network conditions, and browser settings.
  • Cost: Playwright is an open-source automation library, but running browsers still consumes your compute, memory, storage, and operational time. Account for browser installation and execution in the environment that runs the script.

9. Or skip the browser setup

If you need a screenshot without managing a browser process, ScreenshotNeo provides a website screenshot API. One GET request returns an image or PDF. See the 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}`);
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));

ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed. Its 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. Sign up free and get 1,000 screenshots a month with no card.

10. FAQ

Does visible mean every pixel will be unobstructed?

No. It means the element has a non-empty bounding box and is not hidden with visibility:hidden. Another element can still cover it.

Should I use wait_for_selector() in a new script?

Prefer a locator and locator.wait_for(); Playwright documents the older method but discourages it for new code.

How do I capture only the current viewport?

Call page.screenshot(path="shot.png") without full_page=True.

Can I choose a specific browser?

Yes. Launch the browser type you installed through Playwright, and install its matching binary in the runtime environment.