Standard CSS cannot portably select an element because of its rendered text. The often-copied :contains("text") syntax is not standard CSS, so a browser’s querySelector() will reject it. In browser automation, use your framework’s text locator—Playwright’s getByText()—or select a stable attribute, role, ID, or class. XPath is a fallback when no text-locator API exists, but it can become fragile when the DOM changes.
What “select by text” actually means
There are two different problems that are commonly described as “selecting by text”:
- Browser CSS selection: a selector passed to
document.querySelector(), a stylesheet, or a standard CSS engine. - Automation lookup: an API that searches the page’s accessible or rendered content and returns an element handle or locator.
Standard CSS has no general selector for an element’s text content. CSS selectors match the document tree—element names, classes, IDs, attributes, states, and relationships—not the text nodes produced by HTML. Consequently, document.querySelector('div:contains("Welcome")') is not portable browser code.
The :contains() spelling is a non-standard extension associated with an early CSS draft that was removed. A library may implement it, but code depending on it is tied to that library and should not be presented as ordinary CSS.
#1 Best Overall
Use Playwright’s text locator when you are automating a browser
Playwright provides a dedicated text API: page.getByText(). It is a locator, not CSS syntax, and it is the direct solution when the requirement is “find content displaying this text.” Playwright supports substring matching by default, exact-string matching, and regular expressions.
Substring matching
await expect(page.getByText('Welcome, John')).toBeVisible();
The default lookup finds an element whose text contains the supplied string. Use a distinctive phrase or narrow the search to a component so that multiple ancestors do not match.
Exact matching
await expect(page.getByText('Welcome, John', { exact: true })).toBeVisible();
exact: true asks for an exact text value after Playwright normalizes whitespace. Repeated spaces and line breaks are collapsed, and surrounding whitespace is trimmed. “Exact” therefore does not mean byte-for-byte equality with the source HTML.
Regular-expression matching
await expect(page.getByText(/welcome, [A-Z a-z]+$/i)).toBeVisible();
Regular expressions are useful for variable names, dates, or localized fragments. Keep the expression specific enough to avoid matching a large container or an unrelated duplicate.
Scope a text locator to a component
const card = page.locator('.account-card');
await expect(card.getByText('Welcome, John')).toBeVisible();
Scoping reduces accidental matches and makes the intended component explicit. Prefer a stable component hook over a long chain of positional selectors.
Choose role locators for buttons and links
For interactive elements, Playwright recommends role locators because they express the control’s accessible role and name. They are generally more meaningful than searching for a button’s visible text alone.
await page.getByRole('button', { name: 'Save changes' }).click();
await page.getByRole('link', { name: 'Account settings' }).click();
Use getByText() for informational content such as div, span, and p. Use getByRole() for controls such as buttons, links, checkboxes, and headings. This distinction also helps tests continue to describe user-visible behavior when implementation markup changes.
Rank #2
- HTML CSS Design and Build Web Sites
- Comes with secure packaging
- It can be a gift option
Playwright text pseudo-classes are not CSS
Playwright accepts CSS-like extensions including :has-text(), :text(), :text-is(), and :text-matches(). They can be useful inside page.locator(), but they are Playwright features, not selectors that a browser, stylesheet, Selenium implementation, or another framework is required to understand.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
await expect(page.locator('article:has-text("Playwright")')).toBeVisible();
:has-text() matches an element whose own content or descendants contain the substring, case-insensitively after whitespace trimming. Always combine it with a meaningful tag, class, or other constraint. A bare :has-text("Playwright") can match broad ancestors, potentially including body.
Use these extensions only where the code clearly runs through Playwright. If a selector must work in browser JavaScript, CSS, or a different automation tool, use standard selectors or that tool’s documented text API instead.
When standard CSS is required, change what you select
If your code must use querySelector() or a stylesheet, select stable markup rather than text nodes.
Class, ID, and attribute hooks
document.querySelector('#welcome-message');
document.querySelector('.account-card .status');
document.querySelector('[data-testid="welcome-message"]');
An explicit attribute such as data-testid gives tests a contract that is independent of copy changes, translation, and line wrapping. Playwright describes test IDs as resilient when text or role changes, while noting that they are not user-facing locators.
Quick wins for a faster PC:
Repair Windows errors before they cause bigger problemsFix Now →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Clear out junk files and repair common Windows errorsFree Scan →Use semantic HTML where possible
<button type="button" data-testid="save-button">Save changes</button>
<p data-testid="welcome-message">Welcome, John</p>
Semantic elements improve accessibility and make role-based automation possible. Keep visible text for users, but give automated checks a stable hook when the wording is expected to evolve.
Attribute selectors for state or identifiers
document.querySelector('[aria-label="Close dialog"]');
document.querySelector('[data-state="open"]');
Attribute selectors remain standard CSS. Avoid encoding a whole sentence in an attribute merely to imitate text selection; prefer a short, intentional identifier.
Rank #3
XPath when no text-locator API exists
XPath can express text matching in environments that do not provide a dedicated text locator:
//*[contains(text(), 'Welcome')]
This expression checks direct text-node children. Nested markup can defeat that assumption—for example, <p>Welcome, <strong>John</strong></p> splits the visible sentence across nodes. XPath expressions that depend heavily on DOM structure are also vulnerable to harmless layout changes. Playwright supports XPath, but its locator guidance cautions that structure-dependent CSS and XPath can become brittle.
The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →If you must use XPath, prefer a stable attribute or semantic relationship and keep the text predicate narrow:
//p[@data-testid='welcome-message' and contains(normalize-space(.), 'Welcome')]
The . expression considers descendant text, while normalize-space() reduces whitespace differences. Verify the result against your actual markup rather than assuming all visible text is one direct node.
Comparison: which approach fits?
| Approach | Portable outside its framework? | Matching behavior | Best use | Main risk |
|---|---|---|---|---|
| Standard CSS (class, ID, attribute) | Yes | Matches structure and attributes, not text content | Browser code, stylesheets, cross-tool selectors | Requires a stable hook in the markup |
Playwright getByText() |
No; Playwright API | Substring, exact, or regular expression; whitespace normalized | Non-interactive visible content | Ambiguous matches when text is repeated |
| Playwright role locator | No; Playwright API | Accessible role and name | Buttons, links, headings, form controls | Depends on correct accessible semantics |
| Playwright text pseudo-classes | No; Playwright extensions | Substring, exact, or regex variants inside locator syntax | Scoped component queries | Confusing extension syntax with CSS |
| XPath | Usually available in automation tools | Contains, normalized text, and structural predicates | Legacy tools without text locators | Nested text and DOM changes can break it |
data-testid or another explicit hook |
Yes for CSS; API support varies | Exact attribute value | Stable tests for owned pages | Not a user-facing semantic locator |
Common mistakes and how to fix them
“My browser says ‘not a valid selector’”
Cause: a non-standard pseudo-class such as :contains() was passed to querySelector() or a stylesheet.
Fix: replace it with an ID, class, attribute, or test ID. If this is Playwright code, use getByText() or a documented Playwright text extension instead.
“getByText() finds several elements”
Cause: the phrase appears in a parent and one or more descendants, or the page repeats the same label.
Rank #4
- Brand: Wiley
- Set of 2 Volumes
- A handy two-book set that uniquely combines related technologies Highly visual format and accessible language makes these books highly effective learning tools Perfect for beginning web designers and front-end developers
Fix: scope the locator to a component, use exact: true, choose a role locator for controls, or add a stable test ID. Avoid solving ambiguity with nth() unless position is genuinely part of the contract.
“Exact text does not match my source HTML”
Cause: Playwright normalizes whitespace and the browser may split text across nested elements.
Fix: inspect the rendered text, remove accidental line-break assumptions, and use a regular expression only for the variable portion. If exact source structure matters, select a stable attribute instead.
Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteWindows 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 reinstall“The XPath expression misses visible words”
Cause: text() examines direct text-node children; nested elements create separate nodes.
Fix: use contains(normalize-space(.), '...') carefully, or add a test ID and avoid text-dependent XPath.
“A Playwright CSS selector matches the whole page”
Cause: an unscoped :has-text() can match ancestors, including body.
Fix: prepend a component selector such as article or .product-card, then assert or act on the intended descendant.
Best Value
A practical decision process
- Are you writing ordinary CSS or browser JavaScript? Use standard classes, IDs, attributes, or relationships; do not use
:contains(). - Are you testing non-interactive content with Playwright? Start with
getByText(), then scope it if necessary. - Are you clicking or submitting a control? Prefer
getByRole()with its accessible name. - Do you own the markup and expect wording to change? Add a deliberate
data-testidor equivalent stable hook. - Does the tool provide neither text locators nor controllable markup? Use a narrowly scoped XPath expression and account for nested text.
- Will the selector cross tools or languages? Keep it to standard CSS and attributes, or implement a tool-specific adapter rather than sharing Playwright-only syntax.
Or skip the browser setup
If your real goal is to inspect or archive a page image rather than interact with its DOM, ScreenshotNeo returns a screenshot or PDF from one request. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be disabled. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the page verdict and billing status in headers.
Use the API documentation at https://screenshotneo.com/docs/. A cURL 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
The equivalent Python request is:
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 provides an MCP server for AI agents, including Claude and Cursor, with take_screenshot, get_page_info, and capture_pdf tools. Every feature is available on every plan: full-page and element capture, custom CSS and JavaScript, waits, request blocking, cookies and headers, device presets, PDF controls, signed links, asynchronous jobs, bulk capture, caching, and more. 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.
FAQ
Can CSS select text in a stylesheet?
No. Standard CSS cannot conditionally match an element’s rendered text content. Add a class or attribute in the markup if styling depends on that state.
Free tools Windows power users keep installed
One-click scans. No signup required.
Does :contains() work in every browser?
No. It is not a portable CSS selector. A framework or library may implement a similarly named extension, but support and behavior are tool-specific.
Should I use visible text or test IDs in a long-lived test suite?
Use user-facing text or roles when the test is intended to verify what users can perceive. Use an explicit test ID when copy, localization, or wording changes independently of the behavior under test.
Frequently Asked Questions
Can CSS select text in a stylesheet?
No. Standard CSS cannot conditionally match rendered text; add a class or attribute to the markup instead.
Does :contains() work in every browser?
No. It is not portable CSS and only works where a specific library or framework implements an extension.
Do these 3 things before closing this tab:
1Clear out junk files and repair common Windows errors2Scan for outdated or missing drivers - takes under a minute3Repair Windows errors before they cause bigger problemsShould long-lived tests use visible text or test IDs?
Use roles or visible text for user-facing behavior; use a deliberate test ID when wording and behavior change independently.
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.

