ScreenshotNeo

BlogHow-to

How to Click Links Nested in Div and Span Elements with Selenium WebDriver

Find the real anchor inside div and span elements, choose a stable locator, and click it reliably with Selenium WebDriver.

By the ScreenshotNeo team30 September 20268 min read

How to Click Links Nested in Div and Span Elements with Selenium WebDriver

To click a link nested inside <div> and <span> elements, locate the actual <a> element and call click() on it. The surrounding div usually groups content, while the span supplies text or styling. Inspect the live DOM first, then choose the most stable selector that uniquely identifies the anchor.

Selenium supports IDs, CSS selectors, XPath, link text and partial link text. Prefer a stable unique ID; otherwise use a maintainable CSS selector. Use XPath when nested text or a DOM relationship is what distinguishes the link. Selenium’s locator guidance is documented in the official locator documentation.

1. Inspect the HTML before choosing a selector

Start by confirming which element is interactive. A common structure looks like this:

Inspect the DOM, target the anchor, and then perform the click.
Inspect the DOM, target the anchor, and then perform the click.
<div class="container">
  <a href="/reports" class="card-link">
    <span class="label">Reports</span>
  </a>
</div>

The target is the anchor, not the div or the span. If the page instead puts the click handler or an interactive ARIA role on the span, treat that as a different DOM structure and inspect it carefully.

2. Python: complete Selenium examples

Install Selenium with pip install selenium. Recent Selenium releases can manage a compatible browser driver automatically when the browser is installed.

Use a stable anchor ID

from selenium import webdriver
from selenium.webdriver.common.by import By

options = webdriver.ChromeOptions()
options.add_argument("--headless=new")

driver = webdriver.Chrome(options=options)
try:
    driver.get("https://example.com")
    link = driver.find_element(By.ID, "reports-link")
    link.click()
finally:
    driver.quit()

An ID is the simplest choice when it is unique and does not change between releases.

from selenium.webdriver.common.by import By

link = driver.find_element(By.CSS_SELECTOR, "div.container a")
link.click()

This matches an anchor at any descendant depth inside an element with the container class. Narrow it further if the container holds several links:

link = driver.find_element(
    By.CSS_SELECTOR,
    "div.container a.card-link"
)
link.click()
from selenium.webdriver.common.by import By

link = driver.find_element(
    By.XPATH,
    "//div[contains(concat(' ', normalize-space(@class), ' '), ' container ')]"
    "//a[.//span[normalize-space()='Reports']]"
)
link.click()

The .//span condition means the span can be nested anywhere inside the anchor. normalize-space() handles incidental whitespace around the visible label.

from selenium.webdriver.common.by import By

link = driver.find_element(By.LINK_TEXT, "Reports")
link.click()

# For a substring of the anchor text:
link = driver.find_element(By.PARTIAL_LINK_TEXT, "Report")
link.click()

Link-text strategies apply to link elements. They do not locate arbitrary spans. If the anchor contains only an icon and the text exists in a nested span, CSS or XPath is usually clearer.

Check for duplicate matches

from selenium.webdriver.common.by import By

matches = driver.find_elements(By.CSS_SELECTOR, "div.container a")
if len(matches) != 1:
    raise RuntimeError(f"Expected one link, found {len(matches)}")
matches[0].click()

find_element returns the first matching element. A selector that matches several cards can therefore click the wrong link without raising an error. Make the selector more specific or inspect all matches.

3. JavaScript and Node.js Selenium

Install the packages with npm install selenium-webdriver chromedriver. This example uses the same anchor-under-div pattern.

const { Builder, By } = require('selenium-webdriver');
require('chromedriver');

(async function clickNestedLink() {
  const driver = await new Builder().forBrowser('chrome').build();
  try {
    await driver.get('https://example.com');
    const link = await driver.findElement(By.css('div.container a.card-link'));
    await link.click();
  } finally {
    await driver.quit();
  }
})();

For nested text, use XPath:

const link = await driver.findElement(
  By.xpath("//div[contains(@class, 'container')]//a[.//span[normalize-space()='Reports']]")
);
await link.click();

4. Java Selenium example

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;

public class ClickNestedLink {
  public static void main(String[] args) {
    WebDriver driver = new ChromeDriver();
    try {
      driver.get("https://example.com");
      driver.findElement(By.cssSelector("div.container a.card-link")).click();
    } finally {
      driver.quit();
    }
  }
}
Strategy Use it when Example
Unique ID The anchor has a stable unique id. By.ID, "reports-link"
CSS selector The nesting and classes or attributes are stable. div.container a.card-link
XPath Nested text, relationships or conditional attributes identify the anchor. //a[.//span[normalize-space()='Reports']]
Link text The anchor’s complete visible text is stable. By.LINK_TEXT, "Reports"
Partial link text A stable substring identifies the anchor. By.PARTIAL_LINK_TEXT, "Report"

Avoid copied absolute XPath expressions such as /html/body/div[2]/div[1]/a. They depend on incidental layout and commonly break when a wrapper is added. Selenium recommends unique IDs where available, followed by well-written CSS selectors; XPath remains useful when CSS cannot express the condition conveniently. See the Selenium locator guidance.

6. Dynamic pages, overlays and timing

If the link is rendered asynchronously, locate it only after the page state makes it available. If an overlay covers it, diagnose the overlay and page state instead of broadening the selector. A selector can be correct while the element is still not interactable.

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.card-link"))
)
link.click()

Use a wait tied to the element you need, rather than a fixed sleep. Keep the selector narrow so the wait cannot finish on an unintended duplicate.

7. Iframes and shadow roots

Normal document searches do not cross an iframe boundary. If inspection shows the link inside an iframe, switch to that frame before finding the anchor, then return to the default document afterward:

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)
frame = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "iframe.payment")))
driver.switch_to.frame(frame)
try:
    driver.find_element(By.CSS_SELECTOR, "div.container a").click()
finally:
    driver.switch_to.default_content()

For a shadow root, obtain the host element and search its shadow-root context instead of the document context. Selenium documents shadow-root search contexts alongside its element-finding APIs.

8. Troubleshooting checklist

Symptom Likely cause Fix
NoSuchElementException The selector is wrong, the page has not rendered the link, or the link is inside a frame or shadow root. Inspect the live DOM, verify the anchor, add an explicit wait, and switch search context when required.
The wrong link is clicked The selector matches multiple anchors and singular lookup chose the first. Use a unique ID, add a stable class or attribute, or inspect find_elements results.
Text locator finds nothing The text is in a nested span, differs in whitespace, or is not the anchor’s text. Use CSS for the anchor and XPath with .//span[normalize-space()='...'] for nested text.
ElementClickInterceptedException An overlay, cookie banner or another element covers the target. Identify and close the overlay, then wait for the anchor to be clickable.
ElementNotInteractableException The element is hidden, disabled or not ready. Wait for the correct state and confirm that you selected the visible anchor.
Selector breaks after a redesign It depends on generated classes or absolute DOM indexes. Choose a stable ID, semantic attribute, or deliberately scoped CSS/XPath relationship.

9. Performance and reliability

  • Prefer one precise lookup over scanning every anchor on a large page.
  • Use CSS for straightforward structure and XPath only where its relationship or text conditions add value.
  • Keep explicit wait timeouts bounded and record the URL, selector and page state when a wait fails.
  • Use stable test data and selectors that reflect the page’s contract, not its current wrapper depth.
  • After clicking, assert the resulting URL, heading or other page condition so a click on the wrong duplicate cannot pass silently.

10. cURL is not a replacement for a WebDriver click

cURL can send HTTP requests, but it does not run a browser DOM, execute page JavaScript or dispatch a Selenium element click. Use Selenium when you need browser interaction. Use an HTTP client only when the target action is an API request whose endpoint and authentication you control.

Screenshot cleanup removes common overlays before the image is captured.
Screenshot cleanup removes common overlays before the image is captured.

Or skip the browser setup

If your goal is a clean screenshot after navigating a page, ScreenshotNeo provides a single capture request. It accepts the URL and returns PNG, JPEG, WebP or PDF. The API can remove cookie and consent banners, newsletter popups and chat widgets before capture; those cleanup steps can be turned off individually.

Read the ScreenshotNeo API documentation for all options. A minimal request is:

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

Bot checks and CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed as clean shots. Each response reports its result through X-Page-Verdict and X-Billed headers. ScreenshotNeo also offers an MCP server with take_screenshot, get_page_info and capture_pdf tools for Claude, Cursor and other MCP clients. The free plan includes 1,000 screenshots per month without a card; paid plans start at $5 for 3,000 shots.

Create a free ScreenshotNeo account to get started.

FAQ

Should I click the span instead of the anchor?

Usually no. Locate and click the anchor that represents the link. Click the span only when inspection confirms that the span itself owns the interaction.

It is intended for link elements. For nested span text, select the anchor with CSS or XPath.

find_element returns the first match. Make the locator unique or use find_elements to inspect every candidate.

Can a CSS selector express nested visible text?

CSS is useful for structure and attributes. XPath is the practical choice when the nested text itself identifies the anchor.

Switch into the iframe, find and click the anchor, then switch back to the default document.