Locate the real interactive <a> element, not the surrounding <div> or text-only <span>, then call click(). A CSS descendant selector is usually the clearest choice; XPath is better when the link is identified by text inside a nested span.
Find the anchor before you write a selector
HTML commonly groups a link like this:
<div class="container">
<a href="/pricing">
<span class="label">Pricing</span>
</a>
</div>
The div is a container and the span supplies text or styling. The element that represents the link is the anchor. Selenium should normally locate that anchor and click it:
from selenium.webdriver.common.by import By
link = driver.find_element(By.CSS_SELECTOR, "div.container a")
link.click()
Inspect the live DOM in your browser’s developer tools first. Check whether the span is actually inside an anchor, whether the anchor has a stable identifier, and whether several matching links exist. If the page uses a clickable div with a JavaScript handler instead of an anchor, it is a different structure and must be located according to that element’s role and attributes.
Choose the most maintainable locator
| Locator | Example | Best use | Watch for |
|---|---|---|---|
| Unique ID | By.ID, "pricing-link" |
An anchor has a stable, unique id. |
Framework-generated IDs that change between runs. |
| CSS selector | div.container a |
Straightforward nesting and stable classes or attributes. | Overly broad selectors that match a different anchor first. |
| XPath | //div[contains(@class, 'container')]//a[.//span[normalize-space()='Pricing']] |
Nested text, ancestor relationships, or conditions CSS cannot express conveniently. | Copied absolute paths tied to incidental DOM structure. |
| Link text | By.LINK_TEXT, "Pricing" |
The anchor’s visible text is known exactly. | It applies to link elements, not an arbitrary span; whitespace and changing labels can break it. |
| Partial link text | By.PARTIAL_LINK_TEXT, "Pric" |
A stable portion of the anchor text is sufficient. | Common text can match the wrong link. |
Selenium’s singular find_element returns the first match. Narrow a selector until it identifies the intended anchor, or inspect all matches before clicking:
PC 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 & 11Outdated 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 match#1 Best Overall
matches = driver.find_elements(By.CSS_SELECTOR, "div.container a")
if len(matches) != 1:
raise RuntimeError(f"Expected one pricing link, found {len(matches)}")
matches[0].click()
Working Python examples
CSS: an anchor anywhere inside a container
from selenium import webdriver
from selenium.webdriver.common.by import By
driver = webdriver.Chrome()
try:
driver.get("https://example.com")
link = driver.find_element(By.CSS_SELECTOR, "div.container a")
link.click()
finally:
driver.quit()
Replace div.container with the stable container selector from your page. The space in the selector means “an anchor descendant at any depth,” so it still works if another wrapper is inserted between the div and anchor.
XPath: match text in a nested span
from selenium.webdriver.common.by import By
link = driver.find_element(
By.XPATH,
"//div[contains(concat(' ', normalize-space(@class), ' '), ' container ')]"
"//a[.//span[normalize-space()='Pricing']]"
)
link.click()
normalize-space() ignores indentation and repeated whitespace. The descendant expression .//span allows the span to be nested below the anchor rather than requiring it to be an immediate child.
Prefer an anchor ID when one is stable
link = driver.find_element(By.ID, "pricing-link")
link.click()
An ID is generally easier to read and less sensitive to layout changes than a long chain of classes. Do not prefer it if the application generates a new value on every render.
Rank #2
Use link-text strategies only for anchors
exact = driver.find_element(By.LINK_TEXT, "Pricing")
exact.click()
partial = driver.find_element(By.PARTIAL_LINK_TEXT, "Pric")
partial.click()
These strategies inspect the anchor’s link text. If “Pricing” exists only in a nested span but is still the anchor’s rendered text, they can work; if the text belongs to a non-link span, they cannot locate it as a link.
The Tool Desk
Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Wait for dynamic links before clicking
A selector can be correct while the click still fails because the application has not inserted the anchor yet, an overlay covers it, or it is not enabled. Use an explicit wait rather than a fixed sleep:
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)
link = wait.until(
EC.element_to_be_clickable((By.CSS_SELECTOR, "div.container a"))
)
link.click()
element_to_be_clickable waits until Selenium can find a visible, enabled element. It does not prove that the selector is unique or that a site-specific animation has finished, so keep the locator narrow and investigate overlays when clicks are intercepted.
Rank #3
Wait for a nested span’s text
locator = (
By.XPATH,
"//a[.//span[normalize-space()='Pricing']]"
)
link = WebDriverWait(driver, 15).until(
EC.element_to_be_clickable(locator)
)
link.click()
When several components contain the same label, add an ancestor condition such as a menu ID or container class rather than relying on text alone.
Diagnose the usual click failures
The selector finds nothing
- Re-open the Elements panel and verify the class, ID, spelling, and capitalization.
- Confirm that the page has finished rendering. Add an explicit wait for the anchor or its container.
- Check whether the element is inside an iframe. Selenium searches the current document context; a frame must be selected before its contents are searchable.
- Check for a shadow root. A component’s shadow DOM is a separate search context; locate the host, obtain its shadow root, and search inside that context rather than using a document-level selector.
The wrong link is clicked
- Use
find_elementsto count matches and print each candidate’s text andhref. - Add a stable ancestor, data attribute, or unique ID.
- Avoid positional selectors such as
:nth-child(3)unless the position is an intentional, documented contract.
ElementClickInterceptedException
Another element—often a consent dialog, sticky header, loading layer, or modal—is physically above the anchor. Capture a screenshot and inspect the page at the failure point, then dismiss or wait for the obstructing element. Do not immediately replace the click with JavaScript: that can bypass the browser’s real hit-testing and hide a user-visible defect.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
ElementNotInteractableException
The anchor may be hidden, disabled by application state, or outside the visible viewport. Wait for the state that makes it usable, verify that you selected the visible copy, and check whether a menu must be opened before its links become interactable.
StaleElementReferenceException
A framework re-render replaced the node after you located it. Locate the anchor again inside the wait instead of retaining an old element reference:
Rank #4
def click_pricing(driver):
locator = (By.CSS_SELECTOR, "div.container a")
WebDriverWait(driver, 15).until(
EC.element_to_be_clickable(locator)
).click()
The click runs but navigation is not what you expect
Inspect the anchor’s href, target, and click handler. A link with target="_blank" can open a new window or tab; a single-page application may change content without changing the full URL. Assert the result your test actually needs—such as a URL fragment, heading, or application state—rather than assuming every click produces a traditional page load.
Frames and shadow DOM: change the search context
Normal document searches cannot cross an iframe boundary. If inspection shows the anchor inside a frame, switch to that frame before locating it, then return to the parent document when finished:
Quick wins for a faster PC:
Scan for outdated or missing drivers - takes under a minuteDriver Scan →Clear out junk files and repair common Windows errorsFree Scan →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →frame = WebDriverWait(driver, 15).until(
EC.presence_of_element_located((By.CSS_SELECTOR, "iframe.payment"))
)
driver.switch_to.frame(frame)
try:
WebDriverWait(driver, 15).until(
EC.element_to_be_clickable((By.CSS_SELECTOR, "div.container a"))
).click()
finally:
driver.switch_to.default_content()
For a shadow-root component, first locate its host and use the host’s shadow root as the next search context. The exact selectors depend on the component implementation; do not copy a document-level XPath and expect it to cross the boundary.
Best Value
Make selectors resilient in a changing application
- Ask developers for stable IDs or dedicated data attributes when the markup is under your control.
- Anchor a CSS selector to a semantic region, for example
nav.primary a[data-test='pricing'], instead of styling classes that change with a redesign. - Use XPath for a relationship that matters—such as “the anchor whose nested span says Pricing”—but keep it relative and readable.
- Keep locating and clicking close together so a re-render has less opportunity to invalidate the element.
- Record the matched element’s text and URL when diagnosing failures; this quickly reveals duplicate or stale matches.
Or skip the browser setup
If your goal is a dependable image or PDF of a page rather than an interactive Selenium test, ScreenshotNeo makes one request to capture it. Before capture it accepts cookie or consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and each response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
See the ScreenshotNeo API documentation for authentication and options. The following calls are complete examples:
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,
)
r.raise_for_status()
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}`);
It includes full-page captures with lazy images loaded, element-by-CSS capture, dark mode, 12 device presets plus custom viewports, retina scale, PDF paper and page controls, custom CSS and JavaScript, clicks before capture, selector hiding, waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, selectable-TTL caching, signed image links, asynchronous jobs with signed webhooks, bulk capture for up to 100 URLs per call, a usage API, an OpenAPI specification, and compatibility with parameter names used by other screenshot APIs.
| Plan | Included shots | Price |
|---|---|---|
| Free | 1,000 per month | $0, no card |
| Starter | 3,000 | $5 |
| Growth | 15,000 | $15 |
| Pro | 60,000 | $39 |
| Scale | 250,000 | $99 |
| Business | 1,000,000 | $249 |
Every feature is available on every plan, and yearly billing gives two months free. Cookie banners, popups, and chat widgets are removed before the shot; bot checks, blank pages, and failed loads are never billed; an MCP server lets AI agents take screenshots; and 1,000 screenshots a month are free with no card. Create a free ScreenshotNeo account.
Quick verification checklist
- Inspect the live DOM and identify the actual anchor.
- Prefer a stable unique ID; otherwise use a narrow CSS selector.
- Use XPath when nested text or a relationship distinguishes the link.
- Confirm the locator returns exactly the intended element.
- Wait for visibility and enabled state before clicking.
- Investigate frames, shadow roots, overlays, and re-renders when the normal search fails.
- Assert the resulting URL, window, or application state rather than assuming navigation.
Frequently Asked Questions
Does Selenium click the span or the anchor in a nested-link pattern?
Use a selector that returns the anchor. The nested span can identify the anchor in XPath, but the element passed to click() should normally be the link itself.
What happens when several anchors match one selector?
find_element chooses the first match. Use a narrower locator or inspect find_elements and enforce the expected count before clicking.
Can a document-level selector reach a link inside an iframe or shadow root?
No. Those are separate search contexts. Switch into the iframe or search through the shadow host before locating the anchor.
Recommended Free Tools
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.

