Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
page.wait_for_selector(selector, state=..., timeout=...) waits for a matching element to reach a specified state, such as being visible or leaving the DOM. It returns an ElementHandle when a matching element meets the requested state, and returns None for hidden or detached. For new code, Playwright discourages this page method: prefer locator-based waiting or web-first assertions, which fit Playwright’s recommended approach.
What page.wait_for_selector does
The method waits for a CSS selector to satisfy a state condition. If the condition is already true when the call starts, it returns without waiting. Otherwise it polls until the state is reached or the timeout expires. A timeout raises an error; it does not silently return an empty result.
In Python, the method is available on a Playwright Page in both the synchronous and asynchronous APIs. The examples below use Chromium and https://example.com as a simple target; substitute your own page and selector as appropriate.
Recommended Free Tools
Use it in synchronous or asynchronous Python
Synchronous example
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()
The call waits for an h1 with a visible bounding box. It returns an ElementHandle, so you can read from that handle, but locator methods are generally the better choice in new code.
#1 Best Overall
Asynchronous example
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()
In the async API, await both navigation and the wait. Do not mix synchronous Playwright objects with asynchronous calls.
Choose the right state
The state controls what “ready” means. The default is visible; specify a state explicitly when DOM presence or disappearance is what matters.
| State | What Playwright waits for | Typical use |
|---|---|---|
attached |
The element exists in the DOM. It need not be visible. | Confirm that a hidden element or node has been inserted before inspecting it. |
detached |
The element is no longer in the DOM. | Wait until a temporary node has been removed. |
visible |
The element has a non-empty bounding box and is not visibility:hidden. |
Wait for content a user should be able to see. |
hidden |
The element is detached, has an empty bounding box, or is visibility:hidden. |
Wait for a spinner or other visible element to disappear. |
visible is stricter than attached: an element can exist in the DOM while hidden. Conversely, hidden does not prove that the element was removed. If removal specifically matters, use detached.
Do these 3 things before closing this tab:
1Fix the driver behind crashes, sound loss and screen glitches2Repair Windows errors before they cause bigger problems3Scan for outdated or missing drivers - takes under a minuteRank #2
Wait for a spinner to disappear
await page.locator(".spinner").wait_for(state="hidden")
This locator form works in async code; omit await when using the synchronous API. If your requirement is that the spinner node itself be removed, use state="detached" instead.
Prefer locators and web-first assertions in new code
Playwright’s Page API marks page.wait_for_selector as discouraged for new code and recommends locator objects and web-first assertions. A locator describes how to find an element and can resolve it again when the page changes, rather than handing you an element handle that may become stale after a re-render.
Wait with a locator
heading = page.locator("h1")
heading.wait_for(state="visible", timeout=10_000) # synchronous
await heading.wait_for(state="visible", timeout=10_000) # asynchronous
Locator.wait_for supports the same four states and defaults to visible. Use the call matching your API style; the sync and async forms are alternatives, not lines to run together.
Assert visibility or act on the element
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()
Role-, label-, text-, and test-id-based locators often express intent more clearly than a broad CSS selector. Locator actions such as click() automatically wait for the element to be actionable. A web-first assertion such as to_be_visible() retries while checking the condition, rather than asserting once against a momentary page state.
Legacy method versus locator waiting
| Aspect | page.wait_for_selector |
Locator approach |
|---|---|---|
| How it identifies content | Takes a selector string. | Uses a locator, including CSS or semantic locators such as role and label. |
| Result | Returns an ElementHandle for a satisfied visible/attached state; returns None for hidden/detached. |
Locator.wait_for waits without returning an element handle; use the locator for later actions or assertions. |
| States | attached, detached, visible, hidden. |
The same four states. |
| Strict matching | Can use strict=True to require exactly one match. |
Locator actions and assertions operate through locator semantics and auto-waiting. |
| Re-render resilience | Returns a handle to the matched node; that node may no longer represent the current page after re-rendering. | Locators can resolve the target again when used, making them a better fit for changing pages. |
| Assertions | Waits for a selector state, but is not itself a web-first assertion. | Works directly with retrying assertions such as expect(...).to_be_visible(). |
Set a timeout and handle failures
The default timeout for this wait is 30,000 milliseconds (30 seconds). Set a per-call timeout in milliseconds, for example timeout=5000 for five seconds. Set timeout=0 to disable the timeout. Page or context default-timeout configuration can also provide a default for waits.
page.wait_for_selector("#results", state="visible", timeout=5_000)
A timeout means the requested state was not reached before the deadline. It is an exception, so catch it only when your program has a meaningful recovery path; otherwise, let the test fail with its useful diagnostic.
Require exactly one match
page.wait_for_selector(".result", state="visible", strict=True)
With strict=True, more than one match causes an exception rather than choosing one arbitrarily. If the page legitimately contains multiple matches, narrow the selector or use a locator that identifies the intended element. Using .first, .last, or .nth() can be fragile if page order changes.
Why a wait times out—and what to check
- The selector matches nothing. Check spelling, escaping, and whether the element is inside a frame. Confirm the selector against the actual page state rather than assuming the markup is present.
- The node exists but is not visible. If DOM presence is sufficient, wait for
attached. If the element should be visible, inspect whether it is hidden, has no size, or appears only after another interaction. - The page has not reached the application state you expect. Navigate to the right URL and wait for the relevant element or assertion. A navigation completing does not necessarily mean a client-rendered page has finished producing its content.
- The selector matches several elements. Remove
strict=Trueonly if multiple matches are genuinely acceptable; otherwise refine the selector so it points to the intended target. - The timeout is too short for the environment. Use a longer, finite timeout if the operation reasonably needs more time. Do not make every wait infinite to conceal slow or broken behavior.
- The element is replaced during a re-render. Prefer keeping a locator and using it for the later assertion or action instead of relying on a previously returned element handle.
- You are waiting for disappearance but chose the wrong condition. Use
hiddenif the element may be hidden or removed; usedetachedwhen it must leave the DOM.
Do not replace a condition with a fixed sleep
Playwright advises against waiting for a fixed timeout in production: time-based waits make tests flaky. A sleep can be too short on a slow run and unnecessarily long on a fast one. Wait for the selector state, locator assertion, actionability, navigation, or network signal that actually represents the condition your code needs. For example, use await expect(locator).to_be_visible() when visibility is the requirement, rather than sleeping and checking once.
Free tools Windows power users keep installed
One-click scans. No signup required.
Or skip the browser setup
If your goal is a screenshot rather than interacting with or testing the page in Playwright, ScreenshotNeo can capture a URL through one GET request. It does not replace Playwright’s selector wait or provide a way to wait for a specific DOM element; use Playwright when that element-level condition is essential. ScreenshotNeo’s API supports a wait-for-selector option among its capture settings. See the ScreenshotNeo documentation for API parameters.
Best Value
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
The response can be a PNG, JPEG, WebP, or PDF depending on the requested output. ScreenshotNeo removes cookie/consent banners, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks/CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers report the page verdict and billing status. An 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 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. Sign up for ScreenshotNeo’s free plan.
ScreenshotNeo request examples in Python and Node.js
These examples request a screenshot directly; neither is a Playwright selector wait. Keep the API key private and replace the target URL where needed.
The Tool Desk
Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →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}`);
Check the response status and headers before treating a response body as an image in production. The API can identify outcomes such as a cache hit or a page that was not billed; do not assume every response represents a newly billed clean capture.
Frequently Asked Questions
Does `page.wait_for_selector` wait for an element to be clickable?
No. It waits for one of the selector states, not for click actionability. Use a locator’s `click()` when your goal is to click; Playwright automatically waits for actionability.
Can I use `page.wait_for_selector` to wait for text to change?
It waits for selector state, not a particular text value. Use a locator assertion that checks the expected text when the text itself is the condition.
Does `state=”hidden”` mean the element was removed?
No. A hidden element can still be in the DOM. Use `state=”detached”` when removal is specifically required.
Quick Recap
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.

