Selenium: Find Element by Text (XPath, CSS, Link Text) in Python (2026)

Selenium: Find Element by Text (XPath, CSS, Link Text) in Python (2026)

Selenium has no direct By.TEXT locator. To find elements by text: use By.LINK_TEXT for exact link text, By.PARTIAL_LINK_TEXT for partial link text, or XPath //tag[text()='...'] for exact match on any element, or //tag[contains(text(), '...')] for partial match. CSS [value='text'] works for input values. For modern Selenium 4+, driver.find_element(By.XPATH, '//button[normalize-space()="Submit"]') is the most reliable pattern.

Key Takeaways

There is no By.TEXT in Selenium. Text-based finding requires either By.LINK_TEXT, By.PARTIAL_LINK_TEXT, or XPath text() expressions. This is a deliberate design — locating by text is fragile because text changes with i18n and content updates.

XPath text() matches exact text only. //button[text()='Save Changes'] fails if the button has 'Save Changes ' (trailing space). Use normalize-space(): //button[normalize-space()='Save Changes'].

contains(text(), '...') does partial matching. Use when you only know part of the text: //button[contains(text(), 'Save')] matches "Save Changes", "Save Draft", "Auto-Save".

By.LINK_TEXT only works on <a> elements. For buttons, headings, labels — use XPath. For anchors with visible text — By.LINK_TEXT is faster and more readable.

Prefer data-testid over text matching. Text changes break tests. Adding data-testid="save-button" to key elements makes locators resilient to content changes.

Why Selenium Has No By.TEXT

Selenium's locator strategies map directly to browser APIs. There's no native browser API to find elements by text content — getElementById and querySelector work on attributes, not text nodes. Text-based finding requires DOM traversal via XPath.

The simplest text-based locator, but only works for <a> elements:

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

driver = webdriver.Chrome()
driver.get("https://example.com/nav")

# Exact text match — finds <a href="...">Sign In</a>
sign_in = driver.find_element(By.LINK_TEXT, "Sign In")
sign_in.click()

# Case-sensitive: "sign in" will NOT match "Sign In"

For partial text matching on links:

# Finds links containing "Sign" anywhere in text
# Matches: "Sign In", "Sign Up", "Sign Out"
link = driver.find_element(By.PARTIAL_LINK_TEXT, "Sign")

Method 2: XPath text() — Exact Match

For any element type (not just links):

# Exact text match
button = driver.find_element(By.XPATH, "//button[text()='Save Changes']")
heading = driver.find_element(By.XPATH, "//h1[text()='Dashboard']")
label = driver.find_element(By.XPATH, "//label[text()='Email Address']")
span = driver.find_element(By.XPATH, "//span[text()='Success']")

Problem with text(): whitespace matters.

<button>
  Save Changes
</button>
# ❌ Fails — text node has leading/trailing newlines
driver.find_element(By.XPATH, "//button[text()='Save Changes']")

# ✅ normalize-space() strips leading/trailing whitespace
driver.find_element(By.XPATH, "//button[normalize-space()='Save Changes']")
driver.find_element(By.XPATH, "//button[normalize-space(text())='Save Changes']")

Method 3: XPath contains() — Partial Match

# Partial text match — finds button containing "Save" anywhere
button = driver.find_element(By.XPATH, "//button[contains(text(), 'Save')]")

# Practical examples
error = driver.find_element(By.XPATH, "//*[contains(text(), 'Invalid password')]")
status = driver.find_element(By.XPATH, "//div[contains(text(), 'Processing')]")

# More specific with element type
submit = driver.find_element(By.XPATH, "//button[contains(., 'Confirm')]")

Note: contains(., 'text') (dot notation) matches text in the element AND its descendants. Use this for elements with mixed content (text + child elements).

<button><span>Save</span> Changes</button>
# ❌ text() only matches direct text nodes — misses "Changes" which is a text node, but "Save" is in a child span
driver.find_element(By.XPATH, "//button[text()='Save Changes']")

# ✅ . (dot) matches all text content including descendants
driver.find_element(By.XPATH, "//button[contains(., 'Save Changes')]")
driver.find_element(By.XPATH, "//button[normalize-space(.)='Save Changes']")

Method 4: XPath for Specific Contexts

Find by Heading Text

# Find h1 with exact text
h1 = driver.find_element(By.XPATH, "//h1[normalize-space()='Welcome Back']")

# Find any heading level containing text
any_heading = driver.find_element(By.XPATH, "//*[self::h1 or self::h2 or self::h3][contains(., 'Settings')]")

Find by Label Text

# Find label element
label = driver.find_element(By.XPATH, "//label[normalize-space()='Email Address']")

# Find input associated with a label (via 'for' attribute)
label = driver.find_element(By.XPATH, "//label[normalize-space()='Email Address']")
input_id = label.get_attribute("for")
email_input = driver.find_element(By.ID, input_id)

Find by Button Text

# Any clickable element with text
button = driver.find_element(By.XPATH, "//button[normalize-space()='Delete Account']")

# Button or input[type=submit]
submit = driver.find_element(
    By.XPATH,
    "//*[(self::button or self::input[@type='submit']) and normalize-space(.)='Submit Order']"
)

Find Error Messages

# Error message contains specific text
error = driver.find_element(By.XPATH, "//*[contains(@class, 'error') and contains(., 'required')]")

# Alert/toast containing text
alert = driver.find_element(By.XPATH, "//*[@role='alert'][contains(., 'saved')]")

Find Table Cell by Text

# Find td with exact text
cell = driver.find_element(By.XPATH, "//td[normalize-space()='John Smith']")

# Find row containing a cell with text
row = driver.find_element(By.XPATH, "//tr[td[normalize-space()='John Smith']]")

# Get sibling cell in that row (e.g., the status column)
status = driver.find_element(
    By.XPATH,
    "//tr[td[normalize-space()='John Smith']]/td[3]"
)

Method 5: CSS Selectors for Text

CSS has limited text-matching support:

# CSS [value] works for input/button values
input_el = driver.find_element(By.CSS_SELECTOR, "input[value='Submit']")
button = driver.find_element(By.CSS_SELECTOR, "button[value='Save']")

# CSS ::contains() is NOT supported in Selenium (it's non-standard)
# ❌ This does NOT work
driver.find_element(By.CSS_SELECTOR, "button:contains('Save')")

For most text-matching scenarios, XPath is required.

Practical Examples with Waits

Always combine text-based locators with explicit waits on dynamic pages:

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

wait = WebDriverWait(driver, 10)

# Wait for element with text to appear
success_msg = wait.until(
    EC.visibility_of_element_located(
        (By.XPATH, "//*[contains(text(), 'Order confirmed')]")
    )
)

# Wait for button with specific text to be clickable
delete_btn = wait.until(
    EC.element_to_be_clickable(
        (By.XPATH, "//button[normalize-space()='Delete Account']")
    )
)
delete_btn.click()

# Wait for error message
wait.until(
    EC.text_to_be_present_in_element(
        (By.CSS_SELECTOR, ".error-banner"),
        "Invalid credentials"
    )
)

XPath Text Functions Reference

Expression Matches Example
text()='Save' Exact text node content <button>Save</button> only
normalize-space()='Save' Exact text, ignoring whitespace <button> Save </button>
contains(text(), 'Save') Partial text node content <button>Save Draft</button>
contains(., 'Save') Partial text including descendants <button><span>Save</span></button>
normalize-space(.)='Save Changes' Full text with descendants, trimmed Most reliable for complex elements
starts-with(text(), 'Save') Text starts with substring <button>Save & Exit</button>

Find All Elements with Text

# Find ALL elements containing specific text
all_errors = driver.find_elements(By.XPATH, "//*[contains(text(), 'Error')]")
for error in all_errors:
    print(f"Error found: {error.text} (tag: {error.tag_name})")

# Find all buttons
all_buttons = driver.find_elements(By.XPATH, "//button[normalize-space()]")
button_texts = [btn.text for btn in all_buttons]
print(f"Buttons on page: {button_texts}")

# Assert specific text exists anywhere on page
page_text = driver.find_element(By.TAG_NAME, "body").text
assert "Welcome, John" in page_text

Case-Insensitive Text Matching

XPath 1.0 (used by Selenium) has no lower-case() function. Workaround:

# translate() replaces uppercase with lowercase
driver.find_element(
    By.XPATH,
    "//button[translate(normalize-space(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')='save changes']"
)

# Simpler: JavaScript execution for case-insensitive search
element = driver.execute_script("""
    const buttons = document.querySelectorAll('button');
    return Array.from(buttons).find(b => b.textContent.trim().toLowerCase() === 'save changes');
""")

Locating by Multiple Attributes + Text

# Button with specific class AND text
driver.find_element(
    By.XPATH,
    "//button[contains(@class, 'btn-danger') and normalize-space()='Delete']"
)

# Element in specific container with text
driver.find_element(
    By.XPATH,
    "//div[@id='user-actions']//button[contains(., 'Remove')]"
)

# Input button with value
driver.find_element(
    By.XPATH,
    "//input[@type='submit' and @value='Place Order']"
)

Best Practices

Use data-testid when possible

Text-based locators break when text changes:

<!-- Before: "Submit Order" → After: "Place Order" — test breaks -->
<button>Submit Order</button>

<!-- Better: stable test ID, text can change freely -->
<button data-testid="submit-order">Place Order</button>
# ✅ Stable — doesn't break when button text changes
driver.find_element(By.CSS_SELECTOR, "[data-testid='submit-order']")

# ❌ Fragile — breaks when "Place Order" becomes "Submit"
driver.find_element(By.XPATH, "//button[normalize-space()='Place Order']")

Use By.LINK_TEXT for navigation testing

When specifically testing that navigation text is correct:

# Tests both that the link exists AND has the right text
nav_link = driver.find_element(By.LINK_TEXT, "Privacy Policy")
nav_link.click()

Combine with Page Object Model

class CheckoutPage:
    PLACE_ORDER_BTN = (By.XPATH, "//button[normalize-space()='Place Order']")
    SUCCESS_MSG = (By.XPATH, "//*[contains(text(), 'Order confirmed')]")
    ERROR_CONTAINER = (By.CSS_SELECTOR, ".order-error")

    def __init__(self, driver):
        self.driver = driver
        self.wait = WebDriverWait(driver, 10)

    def place_order(self):
        btn = self.wait.until(EC.element_to_be_clickable(self.PLACE_ORDER_BTN))
        btn.click()

    def get_success_message(self):
        msg = self.wait.until(EC.visibility_of_element_located(self.SUCCESS_MSG))
        return msg.text

The Alternative: Natural Language Selectors

Writing XPath expressions for text matching is verbose and fragile. HelpMeTest finds elements the way a human would:

Click the "Place Order" button
Verify the page shows an order confirmation
Click the "Sign In" link

The AI understands "Place Order button" and "Sign In link" — it finds the right element by visual context, not XPath. When "Place Order" becomes "Submit Order", the test doesn't break.

XPath text matching is right when:

  • You need to verify exact text content as part of the assertion
  • Testing internationalization (text changes by locale)
  • Large Selenium codebase where changing the pattern is expensive

When to reconsider:

  • Text changes frequently (marketing-driven button labels)
  • Non-technical team members write the tests
  • Maintenance time for broken locators exceeds feature development

Summary

Find element by text in Selenium Python:

# Links — exact
driver.find_element(By.LINK_TEXT, "Sign In")

# Links — partial
driver.find_element(By.PARTIAL_LINK_TEXT, "Sign")

# Any element — exact text (whitespace-safe)
driver.find_element(By.XPATH, "//button[normalize-space()='Save Changes']")

# Any element — partial text
driver.find_element(By.XPATH, "//button[contains(text(), 'Save')]")

# Any element — text including child elements
driver.find_element(By.XPATH, "//button[contains(., 'Save')]")

# With explicit wait
wait = WebDriverWait(driver, 10)
element = wait.until(
    EC.element_to_be_clickable((By.XPATH, "//button[normalize-space()='Submit']"))
)

Read more