Free tools Windows power users keep installed
One-click scans. No signup required.
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.
The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →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
- 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.
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.
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.
Do these 3 things before closing this tab:
1Clear out junk files and repair common Windows errors2Scan for outdated or missing drivers - takes under a minute3Repair Windows errors before they cause bigger problemsfrom 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.
Rank #4
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 |
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.
Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchPC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Or 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.
Best Value
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.
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.
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.

