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

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:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
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.

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.

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

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.

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_elements to count matches and print each candidate’s text and href.
  • 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.

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

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:

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:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
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.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
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

  1. Inspect the live DOM and identify the actual anchor.
  2. Prefer a stable unique ID; otherwise use a narrow CSS selector.
  3. Use XPath when nested text or a relationship distinguishes the link.
  4. Confirm the locator returns exactly the intended element.
  5. Wait for visibility and enabled state before clicking.
  6. Investigate frames, shadow roots, overlays, and re-renders when the normal search fails.
  7. 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.

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

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.