Free tools Windows power users keep installed

One-click scans. No signup required.

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

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

ElementNotVisibleException means Selenium found the node in the DOM, but the node was not currently visible and could not receive the interaction. In Selenium’s definition, it is “Thrown when an element is present on the DOM, but it is not visible, and so is not able to be interacted with.” A locator succeeding is only a discovery result; it does not prove that the element is rendered, has non-zero dimensions, is unobstructed, enabled, in the right frame, or ready for a click.

Fix the state that prevents interaction rather than replacing the locator at random: wait for visibility or clickability, verify which matching node you selected, remove overlays, switch into the correct iframe, and make the headless viewport deterministic. The workflow below is designed for Selenium with Chrome in CI as well as on a developer workstation.

1. Replace an immediate interaction with a state-based wait

A fixed sleep guesses how long a page will take. An explicit wait polls until the condition you actually need is true, then raises a useful timeout if it never becomes true. Selenium’s visibility condition requires both DOM presence and rendered width and height greater than zero.

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

Wait for an element that must be readable or receive keys

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

wait = WebDriverWait(driver, 15)
field = wait.until(
    EC.visibility_of_element_located((By.ID, "revealed"))
)
field.send_keys("text to enter")

Wait for an element that will be clicked

button = wait.until(
    EC.element_to_be_clickable((By.CSS_SELECTOR, "button.submit"))
)
button.click()

element_to_be_clickable combines visibility with an enabled state. It does not guarantee that a separate modal, backdrop, or animation will not intercept the click, so treat an intercepted click as a reason to inspect page state rather than as proof that the locator is wrong.

#1 Best Overall
Sale
The Web Application Hacker's Handbook: Finding and Exploiting Security Flaws
  • Comes with secure packaging
  • It can be a gift item
  • Easy to read text

2. Verify that the locator selected the intended instance

Modern pages often contain duplicate markup: a hidden desktop/mobile variant, a menu template, or an off-canvas copy. find_element returns the first match, which may be the invisible one.

matches = driver.find_elements(By.CSS_SELECTOR, "button.submit")
print("matches:", len(matches))
for index, candidate in enumerate(matches):
    print(index, {
        "displayed": candidate.is_displayed(),
        "enabled": candidate.is_enabled(),
        "text": candidate.text,
        "size": candidate.size,
        "location": candidate.location,
    })

visible = next((item for item in matches if item.is_displayed()), None)
if visible is None:
    raise RuntimeError("The selector matched no visible button")
visible.click()

If several matches are legitimate, make the selector express the user-visible context: a dialog, a form, a data-testid, or a stable parent relationship. Do not “fix” a hidden duplicate by clicking with JavaScript; that bypasses the interaction semantics your test is supposed to verify.

3. Inspect CSS, dimensions, overlays, and transitions

An element can exist while CSS prevents interaction. Check for display: none, visibility: hidden, zero dimensions, an ancestor that is hidden, a disabled control, or a modal/backdrop covering the target. A fade-in or slide-in transition can also leave a node present before it is usable.

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

Capture computed state at the failure point

element = driver.find_element(By.CSS_SELECTOR, "button.submit")
state = driver.execute_script("""
const e = arguments[0];
const r = e.getBoundingClientRect();
const s = getComputedStyle(e);
return {
  display: s.display,
  visibility: s.visibility,
  opacity: s.opacity,
  width: r.width,
  height: r.height,
  top: r.top,
  left: r.left,
  disabled: e.disabled,
  inViewport: r.bottom > 0 && r.right > 0 &&
               r.top < innerHeight && r.left < innerWidth
};
""", element)
print(state)

Wait for an overlay to disappear

When the application has a known backdrop selector, wait for it to become invisible before waiting for the target click. If the overlay is removed from the DOM, an invisibility condition also handles that case.

wait.until(
    EC.invisibility_of_element_located((By.CSS_SELECTOR, ".modal-backdrop"))
)
wait.until(
    EC.element_to_be_clickable((By.CSS_SELECTOR, "button.submit"))
).click()

Prefer a condition tied to the application’s state—such as a dialog’s disappearance or a loading indicator becoming hidden—over a longer arbitrary sleep.

4. Account for dynamic loading and single-page applications

In a single-page application, a click can trigger a network response that creates or replaces the control. Locate and wait after the state-changing action, not only during the initial page load.

wait.until(EC.element_to_be_clickable((By.ID, "open-settings"))).click()
settings_save = wait.until(
    EC.element_to_be_clickable((By.CSS_SELECTOR, "form#settings button[type=submit]"))
)
settings_save.click()

If a framework replaces a node, keep the locator in the wait instead of retaining an old element reference. A stale reference and an invisible element are different failures, but both are reduced by locating the current node after the update.

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

5. Switch into the iframe before locating its contents

An element inside an iframe is not in the top-level document’s browsing context. Wait for the frame, switch into it, and only then apply your visibility or clickability wait.

frame = wait.until(
    EC.frame_to_be_available_and_switch_to_it((By.CSS_SELECTOR, "iframe.checkout"))
)
card_number = wait.until(
    EC.visibility_of_element_located((By.NAME, "cardnumber"))
)
card_number.send_keys("4242")

driver.switch_to.default_content()

For nested frames, switch one level at a time. Always return to default content before interacting with elements belonging to the parent page.

6. Make headless Chrome’s layout deliberate

Current Chrome uses a unified implementation for headless and headful modes. Selenium enables it with the --headless argument; since Chrome 132, the old Headless mode is available only as a separate chrome-headless-shell binary. You generally do not need a different locator API for headless runs.

What can differ is the environment around the page: viewport size, device scale, fonts, timing, and available resources. Set a known window size and collect artifacts when a test fails.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
from selenium import webdriver
from selenium.webdriver.chrome.options import Options

options = Options()
options.add_argument("--headless")
options.add_argument("--window-size=1440,1000")
driver = webdriver.Chrome(options=options)

Scroll before a supported interaction

An element outside the viewport may be visible in layout but difficult for the browser to interact with. Scroll it into view, then use Selenium’s normal click or key operation.

target = wait.until(
    EC.visibility_of_element_located((By.CSS_SELECTOR, "button.submit"))
)
driver.execute_script(
    "arguments[0].scrollIntoView({block: 'center', inline: 'nearest'});",
    target,
)
wait.until(
    EC.element_to_be_clickable((By.CSS_SELECTOR, "button.submit"))
).click()

Do not use scrolling as a substitute for a missing wait. It changes position; it does not remove a hidden state or an overlay.

7. Build failure diagnostics into the test

A headless-only failure is much easier to explain when the failing run leaves the same evidence you can inspect locally.

from pathlib import Path

try:
    wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "button.submit"))).click()
except Exception:
    Path("failure.png").write_bytes(driver.get_screenshot_as_png())
    Path("failure.html").write_text(driver.page_source, encoding="utf-8")
    print("url:", driver.current_url)
    print("title:", driver.title)
    print("window:", driver.get_window_size())
    print("browser logs may be collected here when enabled")
    raise

Compare the screenshot, HTML, computed state, and viewport with a headed run. Record the Chrome and driver versions in CI logs; a session can start successfully and still expose layout or timing differences after a browser or driver update. Keep those versions aligned.

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.

8. A complete Python pattern

from pathlib import Path
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

options = Options()
options.add_argument("--headless")
options.add_argument("--window-size=1440,1000")
driver = webdriver.Chrome(options=options)
wait = WebDriverWait(driver, 15)

try:
    driver.get("https://example.com/form")
    wait.until(
        EC.invisibility_of_element_located((By.CSS_SELECTOR, ".loading"))
    )
    submit = wait.until(
        EC.element_to_be_clickable((By.CSS_SELECTOR, "button.submit"))
    )
    driver.execute_script(
        "arguments[0].scrollIntoView({block: 'center'});", submit
    )
    submit.click()
except Exception:
    Path("failure.png").write_bytes(driver.get_screenshot_as_png())
    Path("failure.html").write_text(driver.page_source, encoding="utf-8")
    raise
finally:
    driver.quit()

Replace the URL and selectors with your application’s values. If the button is inside an iframe, perform the frame switch before the final wait. If a cookie dialog or chat widget covers it, close or wait for that page state using a selector your application exposes.

9. Diagnose the symptom instead of changing everything

Symptom Likely cause Targeted fix
Locator finds a node, but is_displayed() is false Hidden template, CSS state, or zero dimensions Count matches, choose the intended visible instance, and wait for visibility
Click is intercepted Modal, backdrop, sticky header, or transition Wait for the obstruction to disappear; then wait for clickability
Works headed, fails headless Different viewport or timing Set a fixed window size, capture artifacts, compare computed layout and versions
Element never appears Lazy loading, SPA update, or wrong browsing context Wait after the triggering action and switch into the correct iframe
Only one of several identical controls works Duplicate responsive or off-canvas markup Use a context-specific selector and inspect every match
Session starts, later interactions fail after an update Browser/driver mismatch or changed layout Record and align Chrome and driver versions; recheck viewport assumptions
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

10. Why common “fixes” fail

Increasing a fixed sleep

It may hide a slow run while making fast runs unnecessarily long, and it still provides no guarantee that the required state exists. Replace it with a visibility, clickability, frame, or overlay condition.

Clicking with JavaScript

A script-triggered click can bypass visibility and hit-testing. That can make a test pass without proving that a real user could interact with the control. Use it only when the behavior under test explicitly requires programmatic dispatch.

Changing the locator first

If the selector is correct but the page is still loading, a new selector merely moves the race. First inspect match count, CSS state, dimensions, overlays, and frame context.

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

Or skip the browser setup

If your goal is a reliable page image rather than an interactive Selenium test, ScreenshotNeo makes one GET request and returns PNG, JPEG, WebP, or a PDF. It accepts the cookie or consent banner like a visitor, then removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and whether the request was billed.

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

See the ScreenshotNeo API documentation for request options. It supports full-page captures with lazy images loaded, CSS-selector element capture, dark mode, 12 device presets plus custom viewports, retina scale, PDF paper size/margins/landscape/page ranges, HTML/CSS-to-image, custom CSS and JavaScript, pre-capture clicks, hidden selectors, waits for a selector, delay or network idle, ad/tracker/request/resource blocking, custom headers/cookies/user agent/Authorization, timezone and geolocation, transparent backgrounds, resizing, chosen cache TTLs, signed image links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, a usage API, an OpenAPI specification, and familiar parameter names for easier migration.

An MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients. Every feature is on every plan: 1,000 shots per month free with no card, then Starter $5 for 3,000, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000; yearly billing gives two months free.

Create a free ScreenshotNeo account to get 1,000 screenshots a month without a card.

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

Frequently Asked Questions

When should I use visibility_of_element_located instead of element_to_be_clickable?

Use visibility when the next operation is reading text or sending keys. Use element_to_be_clickable when you are about to click and need the control to be both visible and enabled.

Does headless Chrome require different Selenium locators?

No. Chrome’s current headless and headful modes use the same browser implementation. Investigate viewport, timing, overlays, frame context, and browser/driver versions before rewriting locators.

What artifacts are most useful for a CI-only failure?

Save a screenshot and page source at the exception, print the URL, title, window size, computed display and dimensions, and record Chrome and driver versions. Comparing those with a headed run usually reveals the state difference.

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.