Use an XPath predicate such as //button[contains(., 'Continue')] to find a button whose combined text contains “Continue.” Use text() only when the text is a direct child node, and add normalize-space() when indentation or repeated whitespace can vary.
What XPath contains() actually does
contains() is a string function used inside an XPath predicate. The predicate keeps only nodes for which the selected string includes the substring you provide:
//button[contains(., 'Continue')]
This expression selects button elements whose string value contains Continue. It is a substring test, not an exact-match test: labels such as “Continue,” “Continue to checkout,” and “Continue?” can all satisfy it.
The element name before the predicate matters. An expression such as //*[contains(., 'Continue')] can match the button, its parent form, a card, and other containers that include the same descendant text. Start with the narrowest meaningful element type, then add an attribute or relationship when the page has more than one candidate.
#1 Best Overall
contains(text(), ...) versus contains(., ...)
The most common source of XPath text-locator bugs is choosing the wrong text scope.
| Expression | What it tests | Best use | Typical risk |
|---|---|---|---|
//button[contains(text(), 'Continue')] |
Text nodes selected by text() |
The label is a direct text node of the button | It can miss text split into descendants |
//button[contains(., 'Continue')] |
The element’s combined string value, including descendant text | Labels may contain <span>, icons, or other nested markup |
A broad element can still match because of any descendant text |
//button[contains(normalize-space(.), 'Continue')] |
Combined text after whitespace normalization | Formatting whitespace is inconsistent | It remains a substring match, not an exact label |
//button[normalize-space(.) = 'Continue'] |
Exact comparison after whitespace normalization | You need one normalized label, not a substring | Any additional visible words cause a non-match |
Why nested markup changes the result
Consider this button:
<button><span>Con</span><span>tinue</span></button>
The visible label is “Continue,” but it is distributed across descendant elements. contains(., 'Continue') evaluates the button’s combined string value and is therefore the safer choice. A text() node test addresses text nodes directly; it does not mean “all visible text anywhere below this element.”
When text() is appropriate
If the markup is simply <button>Continue</button>, this is clear and valid:
//button[contains(text(), 'Continue')]
Use it when you have verified that the target text is a direct child and that the page will not wrap part of the label in another element. If the markup can change, contains(., ...) usually tolerates that change better.
Handle spaces and exact labels
HTML often contains line breaks, indentation, or multiple spaces around a label. XPath’s normalize-space() function trims leading and trailing whitespace and collapses runs of whitespace before comparison.
Rank #2
- Used Book in Good Condition
For a partial match that should ignore formatting whitespace:
//button[contains(normalize-space(.), 'Continue')]
For a label that must be exactly “Continue” after normalization:
//button[normalize-space(.) = 'Continue']
Do not use the exact form when the control legitimately includes changing text, such as “Continue to payment.” Use a substring predicate and add another constraint instead.
Quick wins for a faster PC:
Repair Windows errors before they cause bigger problemsFix Now →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Clear out junk files and repair common Windows errorsFree Scan →Use text-based XPath in Selenium
Selenium exposes XPath through the By.XPATH locator strategy. The following examples are complete enough to drop into an existing test after you have created driver.
Python
from selenium.webdriver.common.by import By
button = driver.find_element(
By.XPATH,
"//button[contains(., 'Continue')]"
)
button.click()
For a nested label with irregular whitespace:
button = driver.find_element(
By.XPATH,
"//button[contains(normalize-space(.), 'Continue')]"
)
Java
WebElement button = driver.findElement(
By.xpath("//button[contains(., 'Continue')]")
);
button.click();
Use an exact normalized label in either language when selecting a button with one stable caption:
//button[normalize-space(.) = 'Continue']
Find a different element type
The same predicate works for links, headings, list items, or labels. Change the node test to match the intended control:
//a[contains(., 'Documentation')]
//h2[contains(., 'Account settings')]
//label[contains(., 'Email address')]
Text alone is not a guarantee that an element is actionable. A heading may contain the words you need while the associated link or button is elsewhere. Once you identify the right relationship, express it in the XPath rather than selecting every node with the same words.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Make a text locator specific and maintainable
Add stable attributes when they exist
Text is useful when it is the stable identifying signal, but an id or dedicated data attribute is usually easier to read and debug. Combine it with text when both conditions improve safety:
//button[@type = 'submit' and contains(., 'Continue')]
//*[@data-testid = 'checkout-continue' and contains(., 'Continue')]
//button[contains(@aria-label, 'Continue')]
The final example checks an accessibility label rather than the visible child text. Confirm that the attribute is actually present on the element you intend to click.
Use relationships to disambiguate repeated labels
If a page has “Continue” in several cards, anchor the search to a distinctive section:
//section[@aria-label = 'Payment']//button[contains(., 'Continue')]
//form[@id = 'shipping']//button[contains(., 'Continue')]
When an attribute identifies a container but not the control, use a descendant relationship. When the text belongs to a label and the input is nearby, select the input through the documented relationship rather than assuming the label itself is clickable.
Windows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallOutdated 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 matchCheck that the result is unique
A locator can be syntactically valid and still be unreliable if it matches multiple nodes. In browser developer tools, evaluate the XPath and inspect every result. In Selenium, use a collection while diagnosing:
matches = driver.find_elements(
By.XPATH,
"//button[contains(., 'Continue')]"
)
print(len(matches))
Once the intended control is identified, narrow the expression instead of silently taking the first match. A broad expression such as //*[contains(., 'Continue')] can match containers as well as the control itself.
Common failures and precise fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| No match, although the label is visible | The text is split across nested elements | Replace contains(text(), ...) with contains(., ...) |
| No match because the page is formatted differently | Leading, trailing, or repeated whitespace | Use normalize-space(.); choose substring or exact comparison deliberately |
| Several unexpected elements match | The node test is too broad or text appears in descendants | Use a specific tag, stable attribute, or container relationship |
| The locator matches a similar but wrong control | Substring text is not unique | Add an attribute predicate, a section/form anchor, or use normalized exact text |
| The XPath is valid but the click targets a non-control | A heading, wrapper, or label contains the words | Target the actionable element and use the page’s label-to-control relationship |
| Results differ between environments | The rendered DOM or text differs, or the host’s XPath behavior has not been verified | Inspect the actual DOM in each supported browser and test the exact expression there |
Case and substring assumptions
Do not assume that a differently capitalized label will match your expression. The authoritative material for this implementation does not establish one cross-browser statement about case sensitivity, so verify the behavior for the XPath host and browser combination you support. If capitalization can vary, make that variation an explicit part of your locator strategy and test it against the real DOM.
A practical locator workflow
- Inspect the rendered element. Confirm the tag, visible text, nested markup, and stable attributes rather than copying text from a design mock-up.
- Choose the text scope. Start with
contains(., '...')when descendants may contribute to the label; usetext()only for a direct text node. - Choose substring or exact matching. Use
contains()for a stable fragment andnormalize-space(.) = ...for one normalized label. - Narrow the candidate set. Add the element name, an attribute predicate, or a relationship to a unique container.
- Evaluate the XPath in developer tools. Verify that the result count and selected node are correct in the rendered page.
- Exercise the locator in Selenium. Check that the selected node is present and actionable in the browser state used by the test.
- Recheck after UI changes. A text locator is coupled to wording and DOM structure; update it when either changes and keep the expression specific.
Performance and reliability considerations
For a handful of controls, the important factor is usually selector clarity rather than shaving characters from the XPath. A broad search from the document root asks the browser to consider many nodes and increases the chance of false positives. Scoping to a distinctive section or form reduces both the search space and the debugging work.
Best Value
Text is also presentation content. Product copy, localization, punctuation, and accessibility wording can change without changing the control’s function. Prefer a stable identifier when one exists; use text when it is the dependable signal and protect it with a tag, attribute, or relationship. Keep a test that asserts the expected match count so a new duplicate label fails loudly instead of clicking an arbitrary result.
Or skip the browser setup
If your goal is to capture a page while checking how its rendered text and controls appear, ScreenshotNeo provides a website screenshot API and MCP server. It is separate from Selenium: one GET request returns a PNG, JPEG, WebP, or PDF, and its capture options can wait for a selector, delay, or network idle, click an element, hide selectors, load lazy images, and apply custom JavaScript or CSS. Cookie and consent banners, newsletter popups, and chat widgets are removed before capture; bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the page verdict and billing result.
See the ScreenshotNeo API documentation for the complete option list. A minimal call is:
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}`);
ScreenshotNeo also includes an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. Every feature is on every plan: 1,000 screenshots per month are free with no card; paid plans start at $5 for 3,000 shots, with higher plans at $15 for 15,000, $39 for 60,000, $99 for 250,000, and $249 for 1,000,000. Yearly billing gives two months free. Create a free ScreenshotNeo account to start with the 1,000 no-card screenshots.
Recommended Free Tools
Frequently Asked Questions
Can one XPath contains() expression return more than one element?
Yes. XPath returns every node that satisfies the predicate. Treat a count greater than one as a locator-design problem and add a tag, attribute, or structural relationship rather than relying on whichever result Selenium encounters first.
Should I use a visible label or an accessibility attribute for a text locator?
Use the signal that is stable on the actual control. If the accessible name is exposed through a reliable aria-label, an attribute predicate can avoid fragile descendant markup; verify that the attribute belongs to the element you will interact with.
Why does an XPath copied from a browser inspector stop working in a test?
Inspector-generated paths often describe one current DOM shape, while the test may render a different state, frame, or component structure. Reinspect the rendered test page and rebuild a short expression around stable text, attributes, and relationships.
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.
Do these 3 things before closing this tab:
1Fix the driver behind crashes, sound loss and screen glitches2Clear out junk files and repair common Windows errors3Scan for outdated or missing drivers - takes under a minute

