Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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
Sale
HTML and CSS: Design and Build Websites
  • 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

“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
Sale
Web Design with HTML, CSS, JavaScript and jQuery Set
  • 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

“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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

A practical decision process

  1. Are you writing ordinary CSS or browser JavaScript? Use standard classes, IDs, attributes, or relationships; do not use :contains().
  2. Are you testing non-interactive content with Playwright? Start with getByText(), then scope it if necessary.
  3. Are you clicking or submitting a control? Prefer getByRole() with its accessible name.
  4. Do you own the markup and expect wording to change? Add a deliberate data-testid or equivalent stable hook.
  5. Does the tool provide neither text locators nor controllable markup? Use a narrowly scoped XPath expression and account for nested text.
  6. 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Should 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.

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.