XPath in Selenium
By.xpath in Java, Python, C# and JavaScript, quoting helpers, relative searches from an element, Selenium 4 relative locators, and where XPath sits in a page object.
XPath in Selenium
Selenium hands your XPath to the browser’s own evaluator, so everything on this site applies unchanged. What Selenium adds is the API around it: how you pass the string, how you search within an element, and how you keep locators maintainable across a large suite.
The basic call
// Java
WebElement save = driver.findElement(By.xpath("//button[normalize-space()='Save']"));
List<WebElement> rows = driver.findElements(By.xpath("//table[@id='orders']/tbody/tr"));
# Python
from selenium.webdriver.common.by import By
save = driver.find_element(By.XPATH, "//button[normalize-space()='Save']")
rows = driver.find_elements(By.XPATH, "//table[@id='orders']/tbody/tr")
// C#
IWebElement save = driver.FindElement(By.XPath("//button[normalize-space()='Save']"));
var rows = driver.FindElements(By.XPath("//table[@id='orders']/tbody/tr"));
// JavaScript (selenium-webdriver)
const { By } = require('selenium-webdriver');
const save = await driver.findElement(By.xpath("//button[normalize-space()='Save']"));
const rows = await driver.findElements(By.xpath("//table[@id='orders']/tbody/tr"));
findElement throws NoSuchElementException on zero matches and returns the first match if there are several. findElements returns an empty list on zero matches, which makes it the right call for “assert not present”.
Quoting
XPath strings can use single or double quotes. Pick the one that is not your language’s string delimiter:
By.xpath("//a[@title='Home']") // Java string uses ", XPath uses '
By.XPATH, "//a[@title='Home']"
By.XPATH, '//a[@title="Home"]' # either is fine in Python
When the value itself contains an apostrophe (Don't save), neither works alone. Use concat() or a helper:
static String q(String s) {
if (!s.contains("'")) return "'" + s + "'";
if (!s.contains("\"")) return "\"" + s + "\"";
return "concat('" + s.replace("'", "', \"'\", '") + "')";
}
By.xpath("//button[normalize-space()=" + q("Don't save") + "]");
def q(s: str) -> str:
if "'" not in s:
return f"'{s}'"
if '"' not in s:
return f'"{s}"'
return "concat('" + s.replace("'", "', \"'\", '") + "')"
Never interpolate user-provided text into an XPath without a helper like this; a stray quote breaks the expression.
Searching within an element
WebElement.findElement searches the subtree of that element, but only if the XPath is relative:
WebElement card = driver.findElement(By.xpath("//article[.//h3[normalize-space()='USB-C Hub']]"));
card.findElement(By.xpath(".//button")); // inside the card
card.findElement(By.xpath("//button")); // WRONG: searches the whole document
The leading . is the difference. This mistake is common and silent.
Selenium 4 relative locators
Selenium 4 added RelativeLocator for “the input below this label” style queries:
import static org.openqa.selenium.support.locators.RelativeLocator.with;
WebElement email = driver.findElement(
with(By.tagName("input")).below(By.xpath("//label[normalize-space()='Email']")));
They work on rendered position, so they are useful when the DOM relationship is messy. XPath axes (following::input[1]) do the same job from structure, and are usually more predictable. Use whichever describes the intent more clearly.
Page objects
Keep XPath strings in one place per page, as constants or By fields, with meaningful names:
public class CheckoutPage {
private static final By CARD_NUMBER = By.xpath("//form[@data-testid='checkout']//input[@name='cardNumber']");
private static final By PAY_BUTTON = By.xpath("//form[@data-testid='checkout']//button[normalize-space()='Pay now']");
private By rowRemoveButton(String product) {
return By.xpath("//tr[td[normalize-space()=" + q(product) + "]]//button[@aria-label='Remove']");
}
}
Parameterised locator methods (rowRemoveButton) are where XPath’s text matching shines.
Waits
XPath does not wait. Combine it with WebDriverWait:
new WebDriverWait(driver, Duration.ofSeconds(10))
.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//*[@role='dialog']")));
WebDriverWait(driver, 10).until(
EC.visibility_of_element_located((By.XPATH, "//*[@role='dialog']")))
Frames and shadow roots
Switch into iframes before locating: driver.switchTo().frame(frameElement). For shadow DOM use element.getShadowRoot() and then CSS, because Chrome’s shadow root search context does not support XPath. See Shadow DOM and iframes.
Executing XPath in JavaScript instead
Occasionally you need something WebDriver cannot express, such as counting or reading attribute values in one round trip:
Long count = (Long) ((JavascriptExecutor) driver).executeScript(
"return document.evaluate(arguments[0], document, null, XPathResult.NUMBER_TYPE, null).numberValue;",
"count(//tr[td[normalize-space()='Pending']])");
Common Selenium-specific mistakes
- Forgetting the
.in.//on element searches. - Using
By.xpathforidornamewhenBy.id/By.nameare clearer. XPath is not wrong there, just noisier. - Building XPath with string concatenation and no quoting helper.
- Locating hidden duplicates (mobile nav) and getting
ElementNotInteractableException. Anchor to the visible container.
Next Steps
- XPath in Playwright - The same locators in a locator-first API
- Anchoring to Stable Context - The page-object locator style
- Code Generator - Turn any XPath into Selenium code in five languages