ScreenshotNeo

BlogHow-to

How to Fix Python Selenium Clicking the Wrong Element

Learn why Selenium clicks the wrong element or raises ElementClickInterceptedException, then fix locators, overlays, waits, frames, and rerenders.

By the ScreenshotNeo team1 October 20269 min read

Short answer: first determine whether Selenium found the wrong node or found the right node whose click point is covered. Make the locator unique when the match is wrong. When an overlay, sticky header, modal, animation, or rerender is involved, wait for the page state, remove or dismiss the obstruction, reacquire the element, and then use a normal WebDriver click.

Selenium clicks near the center of an element. If another element covers that point, WebDriver can raise ElementClickInterceptedException instead of clicking through it. A valid CSS or XPath expression can also match a hidden duplicate, a sibling, or a non-actionable container. These are different failures and need different fixes.

1. Identify which failure you have

Observed behavior Likely cause First action
A click reaches a sibling, hidden duplicate, or wrong row Ambiguous locator Count matches and inspect each match’s text, attributes, visibility, and parent
ElementClickInterceptedException An overlay, modal, banner, spinner, sticky header, or animation covers the click point Read the exception’s “other element would receive the click” detail; wait for or dismiss that element
Failure happens only sometimes after AJAX or React/Vue updates Target is not ready or the old node was replaced Wait for the real state change and locate the element again immediately before clicking
Element is found only on some pages or windows Wrong frame, tab, window, or URL Verify URL and switch to the expected browsing context first
Click works only after scrolling Target is under a fixed header or another viewport overlay Scroll it to a clear position and check the center point

2. Use a unique, actionable locator

Do not assume that a syntactically valid selector identifies the intended control. Inspect the matches before changing waits:

from selenium.webdriver.common.by import By

locator = (By.CSS_SELECTOR, "button[data-action='save']")
matches = driver.find_elements(*locator)
print("matches:", len(matches))
for index, element in enumerate(matches):
    print({
        "index": index,
        "text": element.text,
        "tag": element.tag_name,
        "displayed": element.is_displayed(),
        "enabled": element.is_enabled(),
        "aria_label": element.get_attribute("aria-label"),
        "outer_html": element.get_attribute("outerHTML")[:300],
    })

Prefer a stable, unique attribute such as data-testid, data-action, or an accessible role/name. Scope the selector to the correct component, row, or dialog, then select the actionable child:

# Too broad: may match a hidden duplicate
buttons = driver.find_elements(By.CSS_SELECTOR, "button.save")

# Scoped to one dialog and one stable attribute
save = driver.find_element(
    By.CSS_SELECTOR,
    "[role='dialog'][data-panel='profile'] button[data-action='save']"
)

Avoid selecting by changing generated class names or by position alone, such as div:nth-child(4). If a table cell contains an input, locate the input rather than clicking the cell.

3. Wait for readiness and for known obstructions

element_to_be_clickable means that Selenium sees the element as visible and enabled. It does not prove that an overlay is absent from the element’s center, so wait for a known obstruction separately.

from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait

wait = WebDriverWait(driver, 10)
button_locator = (By.CSS_SELECTOR, "button[data-action='save']")
overlay_locator = (By.CSS_SELECTOR, ".loading-overlay")

wait.until(EC.invisibility_of_element_located(overlay_locator))
wait.until(EC.element_to_be_clickable(button_locator))
# Locate again immediately before the action in case the page rerendered.
driver.find_element(*button_locator).click()
wait.until(EC.visibility_of_element_located(
    (By.CSS_SELECTOR, ".save-confirmation")
))

Replace the selectors and post-click condition with the target site’s DOM. Wait on an observable result such as a URL change, confirmation message, or unique element on the next view. Do not use a long fixed sleep as the primary synchronization method. Selenium’s waiting guidance also warns against mixing implicit and explicit waits; configure one strategy consistently.

4. A complete Python example

The following script opens a page, waits for an overlay to disappear, clicks a uniquely identified button, and waits for confirmation. It also captures useful diagnostics when the click is intercepted.

from selenium import webdriver
from selenium.common.exceptions import ElementClickInterceptedException, TimeoutException
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait

URL = "https://example.com/account"
BUTTON = (By.CSS_SELECTOR, "button[data-action='save']")
OVERLAY = (By.CSS_SELECTOR, ".loading-overlay, [aria-busy='true']")
CONFIRMATION = (By.CSS_SELECTOR, ".save-confirmation")

driver = webdriver.Chrome()
wait = WebDriverWait(driver, 10)
try:
    driver.get(URL)
    print("url:", driver.current_url)
    print("title:", driver.title)

    # Confirm that the expected page and frame are active before locating controls.
    wait.until(EC.url_contains("/account"))
    wait.until(EC.invisibility_of_element_located(OVERLAY))
    wait.until(EC.element_to_be_clickable(BUTTON))

    # Reacquire after all waits because a framework may have replaced the node.
    target = driver.find_element(*BUTTON)
    target.click()
    wait.until(EC.visibility_of_element_located(CONFIRMATION))
except ElementClickInterceptedException as error:
    print("click intercepted:", error)
    print("target rect:", driver.execute_script(
        "return arguments[0].getBoundingClientRect().toJSON()", target
    ))
    raise
except TimeoutException:
    print("Timed out at URL:", driver.current_url)
    print("title:", driver.title)
    raise
finally:
    driver.quit()

This is a template: replace the URL, locators, and success condition with the actual application. A timeout value such as 10 seconds is an example, not a performance guarantee.

5. Handle overlays, fixed headers, and animations

Wait for or dismiss the obstruction

Typical obstructions include cookie consent dialogs, newsletter popups, chat launchers, loading masks, and modal backdrops. If the application provides a close control, click it with its own locator and wait for invisibility. If the overlay is controlled by a known state, wait for that state rather than sleeping.

close_locator = (By.CSS_SELECTOR, "button[aria-label='Close']")
modal_locator = (By.CSS_SELECTOR, "[role='dialog']")

if driver.find_elements(*modal_locator):
    wait.until(EC.element_to_be_clickable(close_locator)).click()
    wait.until(EC.invisibility_of_element_located(modal_locator))

wait.until(EC.element_to_be_clickable(BUTTON))
driver.find_element(*BUTTON).click()

Scroll to a clear position

Selenium scrolls an out-of-viewport element into view, but a sticky header can still cover its center. Scroll it to the middle of the viewport and then check its rectangle:

target = driver.find_element(*BUTTON)
driver.execute_script(
    "arguments[0].scrollIntoView({block: 'center', inline: 'nearest'});",
    target,
)
wait.until(EC.element_to_be_clickable(BUTTON))
driver.find_element(*BUTTON).click()

Let animations finish

If a transition moves the control or backdrop, wait for the application-specific class or overlay to settle. A generic sleep can hide the race and make the suite slow. When no reliable state is exposed, use a short, bounded fallback only after inspecting the page behavior.

6. Reacquire elements after rerenders

Single-page applications often replace DOM nodes after validation, route changes, or state updates. A previously stored WebElement can become stale. Locate by the same stable locator after the update:

from selenium.common.exceptions import StaleElementReferenceException

for attempt in range(2):
    try:
        wait.until(EC.element_to_be_clickable(BUTTON))
        driver.find_element(*BUTTON).click()
        break
    except StaleElementReferenceException:
        if attempt == 1:
            raise
        # The next loop reacquires the replacement node.

Do not hide repeated staleness with unlimited retries. Find the event that causes the rerender and wait for its resulting state.

7. Check frames, windows, and navigation

An element can appear to be missing or wrong when the driver is in the wrong browsing context. Verify the URL and switch explicitly:

# New tab or window
original = driver.current_window_handle
for handle in driver.window_handles:
    driver.switch_to.window(handle)
    if "/checkout" in driver.current_url:
        break

# iframe
wait.until(EC.frame_to_be_available_and_switch_to_it(
    (By.CSS_SELECTOR, "iframe[name='payment']")
))
pay_button = wait.until(EC.element_to_be_clickable(
    (By.CSS_SELECTOR, "button[type='submit']")
))
pay_button.click()
driver.switch_to.default_content()

Switch back to the default content before locating elements outside the frame. A navigation or context switch also invalidates assumptions about previously stored elements.

8. Why JavaScript click is usually a last resort

driver.execute_script("arguments[0].click();", target)

This dispatches a DOM click even when a real user could not click the visible control. It can bypass hit-testing, overlays, and parts of the browser’s normal interaction behavior, so it may conceal a production bug or test an unrealistic path. Use it only when the application’s click handler is intentionally the thing under test and you have documented why a WebDriver click cannot represent the required action.

9. Troubleshooting checklist

  • Wrong sibling or hidden duplicate: print find_elements matches, then scope the locator to a stable parent and actionable child.
  • ElementClickInterceptedException: inspect the exception’s covering element; wait for or dismiss the modal, banner, spinner, or animation.
  • Works after manual delay: replace the delay with a condition tied to the actual UI state.
  • Fails after scrolling: account for sticky headers and reposition the target in the viewport.
  • Fails after AJAX or framework updates: wait for the update, discard the old reference, and reacquire the element.
  • Only fails in an iframe or popup: verify the current URL, window handle, and frame before finding the target.
  • Timeouts vary wildly: avoid mixing implicit and explicit waits; use explicit waits around each state transition.
  • Click appears to work but the test continues too soon: wait for the changed URL, confirmation, or next unique element.

10. Performance, reliability, and maintenance

  • Use stable attributes and narrow selectors so lookup stays predictable as the DOM grows.
  • Wait on state changes instead of fixed delays; this reduces idle time while handling slower runs.
  • Keep timeout values bounded and record URL, title, locator, screenshot, and relevant HTML when a failure occurs.
  • Centralize locators and overlay handling so a consent or loading change is fixed once.
  • Do not combine implicit waits with explicit waits because their timeouts can interact unpredictably.
  • Run the same browser version and driver combination in CI and local debugging when investigating rare click differences.

11. Or skip the browser setup

If your goal is to capture a page rather than exercise its controls, ScreenshotNeo provides a single screenshot request. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and the response reports the result in X-Page-Verdict and X-Billed headers.

See the ScreenshotNeo API documentation for all options, including selectors, waits, custom JavaScript and CSS, headers, cookies, device presets, PDFs, caching, async jobs, and bulk capture.

cURL

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

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

An MCP server also lets Claude, Cursor, and other MCP clients call 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. Create a free ScreenshotNeo account.

12. FAQ

Does element_to_be_clickable guarantee a successful click?

No. It checks visibility and enabled status. A separate overlay can still cover the center point.

Should I always use XPath?

No. CSS and XPath both work; choose the selector that uniquely identifies a stable, actionable element.

Why does a click work manually but fail in CI?

CI may have different viewport dimensions, timing, animations, browser versions, or overlays. Record the viewport and wait for the same observable states.

When should I retry?

Retry a bounded, known transient such as one stale reference after a rerender. Do not retry indefinitely or use retries to hide an ambiguous locator.

Can ScreenshotNeo automate a button click?

ScreenshotNeo is for page capture. Its options include clicking an element before capture, custom JavaScript, and waits; use Selenium when you need to validate a full interactive workflow.