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.
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_elementsmatches, 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.


