ScreenshotNeo

BlogHow-to

How to Click a Button with Playwright for Python

Use Playwright’s role-based locators to click buttons reliably in Python, handle strictness and timeouts, verify results, and troubleshoot failures.

By the ScreenshotNeo team1 October 20268 min read

Use a role locator with the button’s accessible name, then call click().

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")
    page.get_by_role("button", name="Continue").click()
    browser.close()

In asynchronous Python, await the same operation:

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

Replace Continue with the button’s accessible name. Playwright recommends user-facing locators such as get_by_role() because they describe the control as a user or assistive technology would identify it. See the official locator guide.

1. Install Playwright and a browser

python -m pip install playwright
python -m playwright install

The second command installs the browser binaries used by Playwright. You can install only a selected browser when your project requires it:

python -m playwright install chromium

2. Complete synchronous example

from playwright.sync_api import sync_playwright, expect

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

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

    # Verify the state produced by the click.
    expect(page.get_by_text("Welcome")).to_be_visible()
    browser.close()

click() performs the action; it does not prove that the application reached the intended state. Follow it with an assertion on a confirmation message, destination, dialog, changed control, or other observable result.

3. Complete asynchronous example

import asyncio
from playwright.async_api import async_playwright, expect

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

        await page.get_by_role("button", name="Continue").click()
        await expect(page.get_by_text("Welcome")).to_be_visible()
        await browser.close()

asyncio.run(main())

Use synchronous Playwright in a synchronous test or script, and asynchronous Playwright when the surrounding application already uses asyncio. Do not mix the two APIs.

4. How Playwright finds the button

Role and accessible name

page.get_by_role("button", name="Sign in").click()

The role is button; the name is the control’s accessible name, commonly its visible text or an associated accessible label. The name is not necessarily the raw HTML value of a class or id.

Case sensitivity and exact matching

# Name matching is commonly substring based.
page.get_by_role("button", name="Sign").click()

# Require the complete accessible name.
page.get_by_role("button", name="Sign in", exact=True).click()

Use exact=True when names such as “Save” and “Save draft” could otherwise overlap.

Scope the locator to a container

If several regions contain an “Add to cart” button, first locate the product or meaningful container, then locate its button:

product = page.get_by_role("listitem").filter(has_text="Mechanical Keyboard")
product.get_by_role("button", name="Add to cart").click()

Scoping makes the intent explicit and prevents a click on the wrong repeated control.

Buttons implemented with other elements

A native <button> is preferable. If a site uses an element with role="button", a role locator can still find it when its accessibility tree exposes that role:

page.get_by_role("button", name="Open menu").click()

If the element has no usable role or accessible name, fix the page’s accessibility markup when you control it. As a fallback, use a stable test contract:

page.get_by_test_id("submit-order").click()

Prefer a test id over a generated CSS class or DOM position when a role and name are unavailable.

5. Locator choices and when to use them

Locator Example Use it when
Role and name get_by_role("button", name="Save") The accessible control name identifies the intended button. This is the default.
Label get_by_label("Enable alerts") The control is associated with a form label.
Text get_by_text("Continue") You need to target visible text and the element is not reliably exposed as a button.
Test id get_by_test_id("continue") You own a stable automation contract.
CSS or XPath locator("button.primary") No user-facing or test-specific locator is available; keep the selector stable.

Avoid choosing .first, .last, or .nth() merely to silence an ambiguity error. Make the locator unique so a page change cannot silently redirect the click.

6. What click() waits for

Before a normal click, Playwright waits for the locator to resolve to exactly one element and checks that it is visible, stable, enabled, and able to receive pointer events. Pointer actions scroll the target into view, wait for the action point to be actionable, and retry when the element detaches during the checks. These behaviors are described in the input guide.

The default locator action timeout is 30,000 milliseconds. Page, context, or locator settings can change it; the Locator API reference documents the available timeout options.

7. Verify navigation or a state change

Assert a destination

from playwright.sync_api import expect

page.get_by_role("button", name="Sign in").click()
expect(page).to_have_url("**/dashboard")

Assert visible content

page.get_by_role("button", name="Save").click()
expect(page.get_by_role("status")).to_have_text("Saved")

Assert a changed button state

page.get_by_role("button", name="Subscribe").click()
expect(page.get_by_role("button", name="Subscribed")).to_be_visible()

Playwright assertions retry until the expected condition is met or the assertion timeout expires. This is more reliable than inserting a fixed sleep. See the assertions guide.

8. Buttons that open a new page, tab, or dialog

New tab or window

with page.expect_popup() as popup_info:
    page.get_by_role("button", name="Open report").click()
report_page = popup_info.value
report_page.wait_for_load_state()
assert "report" in report_page.url

JavaScript dialog

page.once("dialog", lambda dialog: dialog.accept())
page.get_by_role("button", name="Delete").click()

Register the popup or dialog handler before clicking so the event cannot be missed.

9. Troubleshooting click failures

Symptom Likely cause Fix
Strictness violation More than one element matches. Use the exact accessible name, scope to a container, or add a stable test id. Treat the error as a locator design signal.
Timeout waiting for locator The button never appears, the name is wrong, or the page has not reached the expected state. Check the accessible name, wait for the relevant page state, and inspect the locator with count() or the Playwright inspector.
Element is not visible The button is hidden, inside a closed menu, or rendered only after another action. Open the containing UI first and assert visibility before clicking.
Element is not enabled The application keeps the button disabled until validation or loading completes. Complete required fields and wait for to_be_enabled(); do not force the click to bypass business logic.
Another element intercepts pointer events A modal, cookie banner, animation, or overlay covers the target. Close the overlay, wait for the animation to finish, or locate the correct visible button.
Button detaches during click A framework rerender replaced the node. Locate again through the locator API and wait for the stable post-render state; avoid storing an obsolete element handle.
Click succeeds but nothing changes The click was performed, but no outcome was checked, or the handler failed. Assert the expected URL, text, network result, or control state and inspect browser console or application logs.

10. Force clicks and dispatched events

Force a click

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

force=True bypasses non-essential actionability checks, including the normal check that the target receives events. Use it only when bypassing those checks is intentional. It can hide a real overlay, disabled state, or layout problem.

Dispatch a programmatic click

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

dispatch_event("click") triggers programmatic click behavior rather than simulating an ordinary pointer interaction. Use it for tests of event-handler behavior, not as the default fix for a blocked user interaction. The distinction is covered in the official input documentation.

11. Debugging checklist

  1. Confirm the page URL and that the expected page or component has loaded.
  2. Inspect the button’s accessible role and name with Playwright’s inspector or accessibility tooling.
  3. Check the locator count; an action locator should normally resolve to one element.
  4. Check visibility and enabled state before the action when diagnosing a timeout.
  5. Look for cookie banners, dialogs, loading masks, sticky headers, or animations covering the target.
  6. Capture a screenshot and trace during debugging so the failing state is observable.
  7. After the click, assert the expected result instead of relying on a delay.

12. Reliability and performance practices

  • Prefer role and accessible-name locators; they survive many layout and class-name changes.
  • Scope repeated controls to a product card, dialog, table row, or other meaningful container.
  • Keep assertions close to the action so failures identify the broken transition.
  • Use a context or page timeout appropriate to the application, but avoid hiding slow or broken pages with excessive values.
  • Reuse a browser process across tests when your test runner supports it, while isolating state with separate contexts.
  • Use tracing, screenshots, and video selectively in CI to diagnose failures without adding unnecessary runtime or storage.
  • For navigation, wait for the resulting URL or page state; fixed sleeps waste time on fast runs and still fail on slow runs.

13. Or skip the browser setup

If your goal is a clean screenshot after a button-driven page state, ScreenshotNeo provides a single HTTP request instead of maintaining browser setup. Its capture flow can accept cookie or consent banners and remove more than 60 known consent platforms, newsletter popups, and chat widgets before the shot; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and responses identify the result with X-Page-Verdict and X-Billed headers. An MCP server also lets Claude, Cursor, and other MCP clients use take_screenshot, get_page_info, and capture_pdf.

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}`);

See the ScreenshotNeo API documentation for the request options. The free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

14. FAQ

Can I click by visible text?

Yes, but get_by_role("button", name="...") is usually clearer because it verifies that the target is exposed as a button with that accessible name. Use get_by_text() when the element is not reliably represented as a button.

Why does Playwright say the locator matches multiple buttons?

The locator is underspecified. Narrow the accessible name, use exact=True, or scope the locator to the relevant container.

Should I use force=True when a click times out?

Usually no. First identify the hidden, disabled, moving, or covered state that prevents a real interaction. Force is for an intentional bypass.

How do I know the click worked?

Assert the resulting URL, visible confirmation, changed state, dialog, or other application outcome. A completed click() alone is not an outcome assertion.

What is the default click timeout?

The Locator API default action timeout is 30,000 milliseconds unless your page, browser context, or locator configuration changes it.