Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

page.wait_for_selector(selector, state=..., timeout=...) waits for an element matching a CSS selector to reach a requested state. It returns as soon as that state is true, or raises a timeout error if the condition is not met in time. For new code, prefer a locator’s wait_for() method or a web-first assertion: Playwright discourages page.wait_for_selector() for most new uses.

Use page.wait_for_selector in Python

Choose the selector and the condition that matters, then await the method in asynchronous code or call it directly in synchronous code. The following examples navigate to a page and wait for its heading to become visible.

Asynchronous Python

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")

        heading = await page.wait_for_selector("h1", state="visible")
        print(await heading.text_content())

        await browser.close()

import asyncio
asyncio.run(main())

The method returns an ElementHandle when the requested state is reached and the element is present. The example reads the heading’s text from that handle. If your next step is an interaction such as clicking, use a locator action instead; it is generally more resilient to page changes.

Synchronous Python

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")

    heading = page.wait_for_selector("h1", state="visible")
    print(heading.text_content())

    browser.close()

Use one API style consistently: the asynchronous version uses await, while the synchronous version does not. Both use the same state names and default timeout.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Choose the right state

The default state is visible. Visibility means the element has a non-empty bounding box and is not hidden with visibility: hidden; it does not mean the element is necessarily unobstructed or ready for every possible interaction.

State What Playwright waits for When to use it
attached A matching element exists in the DOM, whether visible or not. When presence in the document is enough and the element may be hidden.
visible A matching element has a non-empty bounding box and is not visibility: hidden. When the element needs to be visible before proceeding.
hidden The element is detached, has an empty bounding box, or is visibility: hidden. When waiting for an element to become hidden or disappear.
detached The matching element is no longer in the DOM. When it must be removed from the document, not merely hidden.

For example, an element can satisfy attached while not satisfying visible. Conversely, hidden does not require removal from the DOM: an element with no visible box can satisfy that state. Choose detached when removal itself is the condition you need.

Wait for a spinner to disappear

await page.wait_for_selector(".spinner", state="hidden")

The call completes if the spinner becomes hidden or is removed. For the page method, waits for hidden and detached return None, rather than an element handle.

Set a timeout and handle failures

The default timeout is 30,000 milliseconds (30 seconds). Set a shorter or longer per-call limit with the timeout option; timeout=0 disables the timeout.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
# Wait up to five seconds for a visible element
page.wait_for_selector(".results", state="visible", timeout=5_000)

# In async code:
await page.wait_for_selector(".results", state="visible", timeout=5_000)

If the requested state is not reached before the timeout, Playwright raises a timeout error. A timeout is evidence that the condition was not met within the configured period; it does not by itself tell you whether the selector was wrong, the page failed to reach the expected state, or the wait targeted the wrong condition. Inspect the page and selector before simply extending the timeout.

You can also configure a default timeout for a page or browser context, then override it for a particular wait when needed. A per-call timeout makes the exceptional wait explicit; a shared default can make sense when many operations in the same test need the same limit.

Prefer locators for new code

Playwright’s recommended direction is to use Locator objects and web-first assertions rather than the page-level wait method. Locators resolve elements when used, which is a better fit for pages where content can be replaced or re-rendered. Locator actions such as click() also perform automatic waiting for actionability conditions.

Wait with a locator

# Synchronous
heading = page.locator("h1")
heading.wait_for(state="visible", timeout=10_000)

# Asynchronous
heading = page.locator("h1")
await heading.wait_for(state="visible", timeout=10_000)

The Locator API supports the same four states—attached, detached, visible, and hidden—and defaults to visible. Unlike page.wait_for_selector(), locator.wait_for() does not return an ElementHandle; continue working with the locator.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Prefer role-based assertions and actions when they describe the task

from playwright.async_api import expect

await expect(
    page.get_by_role("heading", name="Example Domain")
).to_be_visible()

await page.get_by_role("button", name="Continue").click()

A role or label describes the control by how a user encounters it, instead of relying on a CSS class that may change during a redesign. Use a test ID when the application provides one specifically for tests. Playwright advises caution with positional selectors such as .first, .last, or .nth(): they can become fragile when the page’s matching elements change.

Keep page.wait_for_selector() when maintaining existing code or when its ElementHandle return is specifically required. Otherwise, express the condition as a locator wait, assertion, or action.

Understand selector matching and strictness

The selector identifies what Playwright checks; the state identifies what must be true of a match. If several elements share a selector, decide whether waiting for any matching element is appropriate. When you need exactly one match, set strict=True. Playwright throws an exception if more than one element matches.

page.wait_for_selector("#account-status", state="visible", strict=True)

# Async equivalent
await page.wait_for_selector(
    "#account-status", state="visible", strict=True
)

Strict matching can expose ambiguous selectors instead of silently accepting an unintended element. If the page legitimately has multiple matches, refine the selector or use a locator that identifies the intended element by role, label, text, or test ID.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Avoid fixed sleeps

Do not replace a meaningful condition with page.wait_for_timeout() in production tests. A fixed delay can be too short on a slow run and unnecessarily long on a fast one. Playwright’s Page API warns that time-based waits make tests inherently flaky.

Wait for the condition that represents progress instead: an element becoming visible, a spinner becoming hidden, an expected navigation, or a relevant network signal. A selector wait is appropriate only when the selector’s state is actually the signal your next step depends on.

Troubleshoot a selector wait that times out

  • The selector matches nothing: Check that the element is in the page you navigated to and that the selector identifies the intended element. Prefer a role, label, or stable test ID when available.
  • The element exists but is not visible: If you only need DOM presence, use state="attached". If visibility is required, inspect why it remains hidden rather than weakening the condition automatically.
  • The element is hidden, but the wait is for removal: detached requires it to leave the DOM. Use hidden if becoming invisible is sufficient.
  • The page is still loading or updating: Confirm that navigation and the action expected to reveal the element have occurred. Wait for a meaningful page or locator condition rather than adding a fixed sleep.
  • The selector matches more than one element: If you enabled strict=True, make the selector more specific or identify the element semantically. Do not reach for .first or .nth() unless the ordering is intentional and stable.
  • The timeout is too short for the expected operation: Set an explicit per-call timeout or adjust the page/context default. Increasing the limit cannot fix a selector that never reaches the requested state.

Or skip the browser setup

If your goal is to capture a webpage after an element appears—not to interact with its DOM in a Playwright test—ScreenshotNeo is a screenshot API with a selector-wait option. Its API parameter names are documented at ScreenshotNeo’s API documentation; the one-call example below captures a page directly, without setting up a browser locally:

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

ScreenshotNeo accepts cookie or 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 or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and responses identify the page verdict and billing status in headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for AI agents using Claude, Cursor, or another MCP client. The free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots. This is for screenshot capture, not a replacement for Playwright when your test needs a browser handle or page interaction.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Sign up for ScreenshotNeo’s free plan: 1,000 screenshots a month, no card required.

Frequently Asked Questions

Does page.wait_for_selector() wait for every image or network request to finish?

No. It waits for the selector to reach the requested element state; that condition is not a general guarantee that all page activity has finished.

Can I use page.wait_for_selector() after a page re-renders?

It can wait for a selector state, but it returns an ElementHandle. For later actions on changing pages, a locator is generally a better fit because it resolves the element when used.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.