ScreenshotNeo

BlogHow-to

How to Use Web Selectors in WebdriverIO

Use WebdriverIO’s `$` and `$$` commands to find elements with CSS, text, XPath, and accessible-name selectors. Learn how to choose stable locators, scope queries, and troubleshoot common issues.

By the ScreenshotNeo team4 October 20269 min read

In WebdriverIO, use $ to locate one element and $$ to locate multiple elements. CSS selectors are the default, and you can also use text selectors, XPath, accessible-name selectors, JavaScript functions in a web context, and custom strategies. Choose a locator that identifies the intended control and is likely to stay stable as the page changes.

The examples below use WebdriverIO’s asynchronous API, as used in current WebdriverIO v9 test code. They assume a configured WebdriverIO test runner and an active browser session. The official Selectors guide describes the available selector strategies.

1. Start with $ and $$

Use $ when you expect one matching element, such as a submit button. Use $$ when you want a collection, such as every row in a table. These are WebdriverIO element-query commands; they are not jQuery or Sizzle.

// One element
const submit = await $('[data-testid="submit"]')
await submit.click()

// Multiple elements
const rows = await $$('.results tbody tr')
console.log(`Found ${rows.length} rows`)

for (const row of rows) {
  console.log(await row.getText())
}

A query returns an element reference or an array of element references; it does not by itself perform an action or assert that the element is usable. Follow the query with the needed command, such as click(), getText(), or an assertion supported by your test setup.

2. Choose a selector strategy

WebdriverIO’s selector strategies serve different needs. Prefer a locator that uniquely identifies the target and reflects how the application is meant to be used. The right choice depends on the markup, test purpose, localization, and browser session.

Strategy Example Useful when Tradeoff
CSS [data-testid="save"] You have a stable test attribute or need a concise structural selector. Generic tags and styling classes may match the wrong element or change during redesigns.
Exact link text =WebdriverIO You need a link by its exact visible text. Text can change with copy edits or localization.
Partial link text *=driver A link contains known text but may include additional words. Partial matches can be ambiguous.
Accessible name aria/Submit The control has a meaningful accessible name and the test targets user-facing behavior. Support and lookup behavior depend on session capabilities; see compatibility notes below.
XPath //ul/li[2] You need to express a relationship in the document tree. Complex expressions can be harder to maintain; classic-session accessible-name lookup uses an XPath approximation.
Custom strategy browser.custom$('byData', 'save') The application has a lookup rule that ordinary strategies do not express well. Requires custom strategy setup and a web environment where execution is available.

CSS is the default selector pattern. WebdriverIO also provides convenient text forms, XPath, and the aria/ strategy. Do not combine different strategies into one selector string; use chained queries when you need to move from a scoped parent to a child with another strategy.

3. Use CSS selectors for stable targets

A dedicated test attribute is a good choice when the application exposes one. An accessible name or visible text can be a stronger expression of user-facing intent when it is unique and stable for your test. Avoid generic selectors like button and selectors based on presentation-only classes unless they are sufficiently specific and intentionally stable.

// Dedicated test attribute
const saveButton = await $('[data-testid="save-profile"]')

// A specific semantic element and attribute
const email = await $('input[name="email"]')

// A user-facing button label (text selector)
const saveByText = await $('button=Save profile')

await saveButton.click()

CSS selection is often the simplest option when the markup includes a stable hook. If a locator uses visible copy, decide whether translation or editorial changes should cause the test to fail. The WebdriverIO best-practices guide recommends resilient selectors and minimizing repeated queries; it also notes that translations may need special handling.

See the official WebdriverIO Best Practices and selector examples for context-specific recommendations. For a user-facing target, the selector guide’s example treats button=Submit as its strongest recommendation; that does not make visible text universally more stable than a test ID.

WebdriverIO supports exact and partial text selector forms for links. The exact form starts with =; the partial form starts with *=.

// Exact link text
const docsLink = await $('=WebdriverIO')

// Partial link text
const partialLink = await $('*=driver')

await docsLink.click()

Use these forms when the link text itself is the behavior you want to verify. Check uniqueness if multiple links could contain the same text. For text that changes by locale, choose a stable test hook or organize the test’s expected text around the application’s translation strategy.

5. Use accessible-name selectors

The aria/ selector targets an accessible name, for example aria/Submit. This can make a test describe a control in terms that are meaningful to users of assistive technology.

const submit = await $('aria/Submit')
await submit.click()

const namedButtons = await $$('aria/Continue')
console.log(namedButtons.length)

Make sure the name is actually exposed and unique in the rendered page. Accessible-name queries differ by session: in BiDi-capable browsers, WebdriverIO first uses browsingContext.locateNodes with an accessibility locator against the browser accessibility tree. If that finds no match, it falls back to a Classic XPath heuristic. Classic sessions use that XPath approximation directly, which the WebdriverIO guide warns can be slower on large pages.

6. Use XPath for document relationships

XPath is useful when the target is best described by its relationship to other nodes. Keep expressions readable, and avoid relying on fragile positions unless position is part of the requirement.

// The second list item
const secondItem = await $('//ul/li[2]')

// A button under a dialog, expressed as a relationship
const dialogAction = await $('//div[@role="dialog"]//button[@type="submit"]')
await dialogAction.click()

When a CSS selector, text selector, or accessible name clearly expresses the target, it may be easier to maintain than a long XPath. Use XPath when its tree relationship is the clearest way to identify the element.

7. Scope a query and combine strategies

Chaining is useful when you first need to select a component and then locate a child inside it. This also lets you use one strategy for the parent and another for the child. The selectors guide cautions that multiple selector strategies cannot be mixed into one selector string.

// Find a calendar component, then its calendar region, then a named control
const selectDate = await $('custom-datepicker').$('#calendar').$('aria/Select')
await selectDate.click()

Keep queries economical. If a single combined selector can clearly identify the target, it can avoid repeated lookups and make the test easier to read. Chain when it adds useful scope, such as selecting a named button within one specific dialog or component.

8. Register a custom locator strategy

For an application-specific lookup rule that standard selectors do not express well, register a strategy with browser.addLocatorStrategy(name, function). Then query through custom$ or custom$$. The documented example uses document.querySelectorAll to return matches.

// Register once while the browser session is available.
await browser.addLocatorStrategy('byTestId', (testId) => {
  return document.querySelectorAll(`[data-testid="${CSS.escape(testId)}"]`)
})

// Use the custom strategy to find one or many elements.
const save = await browser.custom$('byTestId', 'save-profile')
const fields = await browser.custom$$('byTestId', 'profile-field')

await save.click()
console.log(fields.length)

The function runs in a web context, so this approach requires a web environment where execute can run. Escape interpolated values when building CSS selectors, as in the example, and keep the strategy focused on a clear application rule.

Reference: Browser addLocatorStrategy API, Browser custom$ API, and Element $ API.

9. Shadow DOM and version compatibility

In WebdriverIO v9, selectors automatically pierce Shadow DOM. The current selectors guide says the special >>> deep selector is no longer required; remove that prefix when migrating to v9.

Selector behavior also depends on the active browser session. In particular, aria/ can use the BiDi accessibility-tree locator when available and otherwise uses the documented XPath approximation. If a selector works in one session configuration but not another, check the session type and the WebdriverIO version before changing a sound locator.

10. Troubleshoot selector failures

Symptom Likely cause What to check or change
No element found The selector does not match the rendered markup, the page has not reached the expected state, or the target is inside a component or shadow tree. Inspect the current DOM and selector spelling; wait for the intended page state using your test’s supported wait pattern; scope within the component. In v9, remember Shadow DOM is pierced automatically.
More than one element matches A text, partial-text, generic CSS, or XPath selector is ambiguous. Make the selector more specific, scope it to a parent, or use a dedicated test ID. Use $$ only when a collection is intended.
Wrong element is selected after a redesign The selector relies on styling classes, generic tags, or incidental markup structure. Use a stable test attribute or a meaningful accessible name; adjust the locator to reflect the intended behavior.
Text locator stops working after a locale change The visible copy is translated or edited. Use a stable test hook, or use the expected localized text deliberately and maintain it with the application’s translation approach.
aria/Name does not match as expected The element has no such accessible name, or session capabilities select a different lookup path. Inspect the accessible name and check whether the session is BiDi-capable or Classic. The Classic XPath approximation can be slower on large pages.
Legacy deep selector fails or is redundant A test still uses the >>> prefix after moving to v9. Remove the prefix; v9 automatically pierces Shadow DOM.
Custom strategy cannot access the page The strategy needs a web execution context but the current environment does not support execute. Confirm the environment and session support the web-context operation, or use a built-in locator strategy.

11. Performance, reliability, and cost

There is no universal performance ranking for CSS, text, XPath, and other selector forms. Locator cost depends on the page and session. The WebdriverIO selector guide specifically notes that BiDi accessibility-tree lookup is typically faster than its Classic XPath approximation, while the latter may be slower on large pages.

For reliable tests, use a unique target, keep selectors tied to stable application meaning, and avoid repeated queries when one scoped or combined query is clear. If a locator is flaky, first check whether the page state and target uniqueness are deterministic; switching selector syntax alone will not fix a race in the page.

WebdriverIO is an open-source automation framework; the cited selector documentation does not specify per-query pricing. Your operational cost depends on the browser and test infrastructure you run. If the goal is to save a page image rather than interact with it in a test, a screenshot API can avoid setting up a browser automation session.

Or skip the browser setup

For a one-request screenshot rather than an interactive selector test, ScreenshotNeo returns an image or PDF from a URL. Its capture can accept cookie and consent banners and remove more than 60 known consent platforms, newsletter popups, and chat widgets before the shot; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and the response identifies the page verdict and billing status in headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for AI agents.

cURL:

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}`);

See the ScreenshotNeo API documentation for request options. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up for free.

FAQ

Are $ and $$ jQuery commands?

No. They are WebdriverIO element-query commands: $ locates one element and $$ locates multiple elements.

Can I put an XPath and an accessible-name selector in one string?

No. Use separate chained queries when you need to change strategies between a scoped parent and its child.

Do I still need >>> for Shadow DOM in v9?

No. WebdriverIO v9 automatically pierces Shadow DOM, so the special deep-selector prefix is no longer required.

Which locator should I choose first?

Start with a unique, stable identifier that expresses the test’s intent: often a dedicated test ID, or an accessible name or user-facing label when that is the behavior being tested.