I’ve lost count of how many UI tests fail because a button moved, a class name changed, or the markup got “cleaned up” by a front-end refactor. Yet the button’s visible text usually stays stable, because product teams care about the words a user sees. If you can click by text, you can make tests that survive most layout tweaks. That’s why I keep a text-first strategy in my Selenium toolkit, even in 2026 when we have richer selectors and AI-assisted test generation.
You should expect a few tradeoffs: text can be localized, A/B tested, or nested inside spans, and Selenium’s older convenience methods are now deprecated. Still, a solid text-based click flow is one of the quickest ways to get reliable coverage for critical paths like Sign In and Checkout. I’ll walk you through how I do it today with Python and Selenium 4, how to avoid the traps that cause flaky clicks, and how to decide when text is the right selector and when it isn’t. You’ll see a fully runnable script, modern waiting patterns, and practical fallback selectors for real-world pages.
Why text-based clicks still matter
I treat text as the closest thing to a stable contract between design and automation. Class names are a styling detail; text is a product decision. If the words on a button change, you want the test to break, because the user-facing behavior changed. That makes text an honest selector.
Text-based clicks also help when you’re dealing with templated UI components that generate unpredictable IDs. If a component library gives you random suffixes like button-7843, you’re stuck without a stable attribute. Text gives you a clean handle that matches the user’s mental model.
That said, text is not always the safest choice. If the UI is localized, you must handle multiple languages. If the app uses an icon button with no visible text, you need an alternate selector. I think of text selectors as a first option for visible, user-facing buttons, and a fallback to attribute or ARIA selectors when the UI is hidden, localized, or dynamic.
Here’s the rule I follow: if a user can read it and a product manager might change it, it’s a good test trigger. If a user cannot read it, text is fragile. That mindset keeps me from overusing text where it doesn’t belong.
Selenium 4 setup and what changed since older guides
If you learned Selenium years ago, you might remember methods like findelementbylinktext. In Selenium 4, those convenience methods are deprecated. You now use the unified find_element with a By strategy, which is more consistent and easier to read once you get used to it.
You should install Selenium with pip:
pip3 install selenium
In 2026, Selenium Manager is built in and can download browser drivers for you automatically. I still pin drivers in CI for determinism, but locally I let Selenium Manager handle it because it saves time. If you prefer manual installs, download the driver that matches your browser version and put it on PATH, or pass the driver path explicitly.
Here’s the modern pattern for link text in Selenium 4:
from selenium.webdriver.common.by import By
element = driver.findelement(By.LINKTEXT, "Sign In")
If the button is not a link but a real
element = driver.find_element(By.XPATH, "//button[normalize-space(.)=‘Sign In‘]")
I recommend normalize-space because it trims extra whitespace that can appear due to layout or CSS.
Choosing the right text selector: link text, button text, and hybrid strategies
There are three common cases in real apps:
1) Anchor links: The control is an tag with visible text, like a navigation link or sign-in link.
2) Button elements: The control is a
3) Custom components: The control is a div or span with role="button" or an ARIA label.
For anchor links, use By.LINKTEXT or By.PARTIALLINK_TEXT. I only use partial text when the full text is dynamic, like “Continue as Maria”. Otherwise, partial text is a recipe for accidental matches.
For button elements, XPath is usually the most direct:
//button[normalize-space(.)=‘Sign In‘]
If the button wraps the text in spans, XPath still works because . captures descendant text nodes. If the text is split across multiple spans with extra whitespace, normalize-space helps keep things stable.
For custom components, I prefer accessibility attributes because they are meant to be stable. An ARIA label is a great selector:
//*[(@role=‘button‘ or self::button) and @aria-label=‘Sign In‘]
If you can influence the app, ask the front-end team to add data-testid attributes. That’s the most stable selector of all. But when you can’t, text is still a practical fallback.
Here’s a quick Traditional vs Modern comparison I use when training teams:
Traditional
—
findelementbylinktext("Sign In")
time.sleep(10)
manual chromedriver path
raw NoSuchElementException
A runnable script that clicks by text the right way
I’ll start with a clean script that you can run end-to-end. It uses explicit waits, a modern Selenium 4 style, and falls back cleanly if the button is missing. You can adapt this to your site by changing the URL and the text.
Python (runnable):
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException, NoSuchElementException
def clickbytext(driver, text, timeout=10):
"""Click the first element that matches visible link or button text."""
wait = WebDriverWait(driver, timeout)
# Try link text first for anchor-based controls.
try:
element = wait.until(EC.elementtobeclickable((By.LINKTEXT, text)))
element.click()
return "link_text"
except TimeoutException:
pass
# Fall back to button text via XPath.
try:
xpath = f"//button[normalize-space(.)=‘{text}‘]"
element = wait.until(EC.elementtobe_clickable((By.XPATH, xpath)))
element.click()
return "button_xpath"
except TimeoutException:
raise NoSuchElementException(f"No clickable element found with text: {text}")
def main():
# In 2026, Selenium Manager can auto-resolve the driver.
driver = webdriver.Chrome()
try:
driver.get("https://example.com")
driver.maximize_window()
strategy = clickbytext(driver, "Sign In", timeout=12)
print(f"Clicked using: {strategy}")
finally:
driver.quit()
if name == "main":
main()
What’s happening here:
- I try link text first because it’s the cleanest and most precise for navigation links.
- If that fails, I fall back to a button XPath that matches visible text.
- I use WebDriverWait instead of sleep to avoid slow and flaky tests.
- I raise NoSuchElementException to keep failure signals consistent with Selenium’s native behavior.
If you’re working with a UI where the button appears after a loading animation, increase the timeout or wait for a specific state, like a menu opening or a modal becoming visible. I avoid sleeping because it creates race conditions and wastes time when the element appears quickly.
Dealing with edge cases: localization, nested text, and invisible elements
Text selectors can be fragile in a few specific situations. Here’s how I handle them.
Localization: If your UI supports multiple languages, don’t hard-code text. Read the expected label from the locale file or a test data fixture. For example, store labels in a JSON map keyed by locale, then pass the correct string into clickbytext.
Nested text: Modern design systems often wrap text in spans for icons or styling. XPath with normalize-space handles most cases, but if you find text split with hidden nodes, use contains instead of exact match:
//button[contains(normalize-space(.), ‘Sign In‘)]
Be careful with contains, though. It can click the wrong button if multiple elements share similar text. I only use it when the exact text is dynamic, like “Continue as Maria”.
Invisible elements: Selenium might find elements that are present in the DOM but hidden. If you skip the elementtobe_clickable condition and click immediately, Selenium can throw ElementNotInteractableException. The wait condition avoids that by checking visibility and clickability.
If you still get visibility errors, check for overlays. A modal or sticky banner can intercept the click even if the element is technically clickable. In that case, wait for the overlay to disappear or scroll the element into view before clicking.
Frames and shadow DOM: If the button is inside an iframe, you must switch to that frame first:
driver.switch_to.frame("auth-frame")
For shadow DOM, Selenium 4 supports shadow root access, but text selectors inside shadow roots require you to pierce the shadow boundary first. If you control the app, I suggest adding test IDs instead of relying on shadow DOM text.
When text selectors are the wrong tool
I’m opinionated here: don’t use text selectors for elements that change frequently due to experimentation or personalization. That makes tests noisy. Instead, use data-testid or ARIA labels that stay stable while the visible text varies.
Here are a few cases where I switch away from text:
- A/B tests that swap button labels weekly.
- Buttons where the text includes user data, like “Pay Maria $42.00”.
- Icon-only buttons, where the label is in a tooltip or aria-label.
- Pages with multiple buttons that share the same text, like multiple “View” buttons in a list.
In those cases, choose a selector that encodes intent. A data-testid like checkout-submit is perfect. If you can’t modify the app, a scoped selector plus text can work: find a container with a known ID, then find the button inside it by text. That reduces ambiguity without abandoning text entirely.
Avoiding common mistakes that create flaky tests
Most flaky clicks come from timing, not selectors. Here’s the checklist I use:
- Don’t call time.sleep unless you’re debugging and need a pause to inspect the UI. Replace it with WebDriverWait so tests run fast when the page is quick and wait only when needed.
- Don’t rely on partial text unless you have a strong reason. Partial matches can click the wrong element and hide bugs.
- Don’t assume the first match is the right one. If the page has multiple elements with the same text, narrow the search by a container or use a more specific XPath.
- Don’t forget to wait for navigation or state change after the click. If you click and immediately assert a new page, you can race the transition. Wait for a new element or URL change.
- Don’t ignore exceptions. Wrap your click in a small helper that logs the page URL and takes a screenshot on failure. That saves time when you debug in CI.
Here’s a small helper I use in larger suites to add visibility when a click fails:
from pathlib import Path
def safeclickbytext(driver, text, timeout=10, screenshotsdir="./screens"):
try:
return clickbytext(driver, text, timeout=timeout)
except Exception as e:
Path(screenshotsdir).mkdir(parents=True, existok=True)
driver.savescreenshot(f"{screenshotsdir}/click-failed.png")
raise e
The screenshot alone often tells you whether the element was missing, hidden, or covered.
Performance and stability tradeoffs you should expect
Text-based lookups are generally fast, but XPath on large pages can add overhead. In my experience, a straightforward XPath lookup typically completes in the 10–40ms range on local runs, but it can be slower on heavy pages or in remote browsers. That’s still acceptable for functional tests, but it’s a reason to keep selectors specific.
If you need a large number of clicks on a page, consider pre-scoping your search to a container. For example, find the sidebar element by ID, then call .find_element within it. That reduces DOM scanning and avoids accidental matches.
Also be mindful of implicit waits. I recommend explicit waits only. Implicit waits can make every find call pause for the full timeout, which hides real performance problems and makes debugging slower. Explicit waits give you a clear place to adjust timing when a page grows slower in CI.
For CI reliability, prefer headless mode with a consistent window size. I use 1365×768 or 1920×1080 to avoid responsive layout surprises. If your tests depend on a specific layout, set the size explicitly so the text you target remains visible.
Modern workflows in 2026: pairing Selenium with AI-assisted testing
These days I often sketch a test in a prompt and let an AI assistant draft the first pass. That gets me a quick scaffold, but I still review selectors by hand. Text selectors are simple to generate, yet they can be brittle if the assistant guesses the wrong label. So I validate the UI text against the actual app or a design spec.
I also use small helpers to reduce repetitive code and keep tests readable. A clickbytext utility is a good example. Keep it in a shared module so every test can call it consistently. That way, if you decide to change the selection strategy later, you update one place.
For teams with mixed skill levels, I suggest one clear pattern for text clicks and document it in your test guidelines. Consistency beats cleverness. When a test fails at 2 a.m., you want the selector logic to be predictable.
If you use visual testing tools, text selectors become even more useful. You click the button by text, then assert that the resulting view matches a baseline. It’s a clean, human-readable flow that aligns with how product teams describe user journeys.
Closing: what I want you to take away and what to try next
Text-based clicks are still one of the most practical ways to automate user flows, especially when you need tests that behave like a real person. I recommend you start with visible text for links and buttons, use explicit waits to avoid flakiness, and fall back to ARIA labels or test IDs when the UI is dynamic or localized. The modern Selenium 4 style is straightforward: use By.LINK_TEXT for anchors and XPath for button text, and wrap it in a small helper so you can evolve your approach without touching every test.
If you want to put this into practice right away, take a page in your app that has a stable “Sign In” or “Continue” control and build a tiny smoke test around it. Add a helper that records a screenshot on failure so you can debug quickly in CI. Then, find one place where text is a bad selector and replace it with a test ID, just to see the contrast in stability. That exercise makes the tradeoffs obvious.
I also suggest you keep a short list of approved selectors for your team: text for visible user actions, ARIA labels for icon-only controls, and data-testid for anything else. When you use the right selector for the right job, Selenium stops feeling flaky and starts feeling like a reliable signal on every release.


