ScreenshotNeo

BlogHow-to

How to Use Web Selectors in Cypress

Choose reliable Cypress selectors with data attributes, text queries, and accessible roles. Learn how to scope, retry, and troubleshoot DOM queries.

By the ScreenshotNeo team4 October 202610 min read

Cypress selectors identify elements in the application under test. Use cy.get() with a dedicated data-* attribute when the test should survive styling and wording changes; use cy.contains() when the wording itself matters; and use role queries when the test should target accessible semantics. Scope duplicate matches with .within() or .find(), then assert the page state you expect.

This guide covers Cypress end-to-end and component tests. Cypress commands query the app’s DOM; they do not select elements in the Cypress runner UI.

1. Choose a selector that matches the test’s purpose

Ask whether a change to the element’s visible content should fail the test. If yes, select by text. If no, use a dedicated test attribute. Cypress recommends test-specific data attributes because they are independent of CSS and ordinary JavaScript refactors. Its examples include data-cy, data-test, data-testid, and data-qa. Pick one project convention and use it consistently.

Selector approach Use it when Trade-off
data-cy or another dedicated data-* hook You need a stable target and a wording or style change should not break the test. The application markup must include and maintain the hook.
cy.contains() The displayed text is part of the behavior being tested. Copy changes can break the test; string matches are substrings.
Accessible role and name The test should locate a control as users of assistive technology identify it. Requires Cypress Testing Library and suitable accessible semantics.
CSS class, ID, or tag A semantic or test hook is unavailable and the chosen selector is stable enough for the test. Classes and generic tags often change for reasons unrelated to behavior; IDs can be reused or changed.

Cypress’s best-practices guidance puts generic tags and styling classes below purpose-built hooks. Its rule of thumb is to use text when a content change should fail the test, and a data attribute otherwise. See Cypress best practices.

Add a test hook to the application

<button data-cy="submit-order">Place order</button>
// The test targets the control independently of its label and styling.
cy.get('[data-cy="submit-order"]').click()

The CSS attribute selector is ordinary CSS; cy.get() runs it against the application document. The hook does not change the element’s behavior or require a Cypress-specific markup feature.

2. Find elements with cy.get()

Use cy.get(selector) for CSS selectors that can yield one or more elements. In ordinary use it starts at the Cypress root, usually the application document. It also retrieves aliases with cy.get('@alias').

// One element by a dedicated hook
cy.get('[data-cy="submit-order"]').should('be.enabled')

// A collection matching a CSS selector
cy.get('input, textarea, select').should('have.length', 3)

// A collection of repeated items
cy.get('[data-cy="todo-item"]').should('have.length', 5)

Use valid CSS selector syntax inside cy.get(). Quote attribute values when they contain characters that need quoting, for example [data-cy="order:submit"]. For an alias:

cy.get('[data-cy="results"]').as('results')
cy.get('@results').should('be.visible')

A DOM alias normally reruns the queries that created it when retrieved, which helps Cypress work with a changing DOM. A static alias keeps the value captured when it was created. Choose the alias type deliberately if the page updates between commands. See the cy.get() API.

3. Match meaningful text with cy.contains()

Use cy.contains() when the text is what the test intends to verify or operate on. It accepts a string, number, or regular expression, and yields at most one element. A string matches a substring, so cy.contains('Save') can match “Save draft.” To require exact text, use an anchored regular expression.

// Substring match
cy.contains('Save').click()

// Match a button whose complete text is Save
cy.contains('button', /^Save$/).click()

// Text identifies the row; then select its action within that row
cy.contains('tr', 'Jane').contains('button', 'Edit').click()

The optional first selector limits the candidate element type. This is useful because Cypress may yield a preferred interactive ancestor, such as a button or link, instead of the deepest element containing the text. Specify 'button' or 'a' when the intended element type matters.

// Avoid matching a similar control elsewhere on the page
cy.get('[data-cy="confirm-dialog"]').within(() => {
  cy.contains('button', 'Yes, Delete!').click()
})

cy.contains() can find hidden elements. If visibility is part of the behavior, assert it explicitly with .should('be.visible'). For exact text, account for whitespace from nested markup and use a regular expression appropriate to the rendered text. See the cy.contains() API.

4. Scope queries with within() and find()

A plain cy.get() generally starts again at the Cypress root, even when written after another command. Use .find() to query descendants of the current subject, or .within() to make the callback’s Cypress queries search inside a container.

// Scope several queries to one dialog
cy.get('[data-cy="confirm-dialog"]').within(() => {
  cy.get('button').should('have.length', 2)
  cy.contains('button', 'Delete').click()
})

// Continue from the current subject and select descendants
cy.get('[data-cy="profile"]')
  .find('input')
  .should('have.length', 2)

Use .within() when several operations share the same container. Use .find() for a descendant query in a chain. Scope first when repeated labels or controls appear in different parts of the page; this makes both the intended target and the test’s boundary clearer.

5. Query by accessible role and name

When the test should identify a control through its accessible role and name, Cypress documents queries from Cypress Testing Library. For example:

cy.findByRole('button', { name: 'Submit' }).click()

This is useful when the control’s role and accessible name are part of what the test exercises. A dedicated test attribute serves a different intent: it locates a stable test hook without making the visible wording the selector. Both approaches can coexist in a suite. Follow the Cypress accessibility testing guide for setup and supported queries.

6. Let Cypress retry queries and assertions

Cypress queries retry while seeking matching elements, and retry chained assertions until they pass or the applicable timeout expires. Express the expected state as a query and assertion instead of adding a fixed sleep for normal page rendering.

// Wait for the app to render the success state
cy.get('[data-cy="saved-message"]').should('be.visible')

// A text query can also wait for expected content
cy.contains('[data-cy="status"]', 'Saved').should('be.visible')

cy.contains() accepts a timeout option. Use a longer timeout only when the application legitimately needs more time for that state. For transient messages, first assert that the action caused the message to appear if that matters; an immediate not.exist assertion can pass before the message ever appears.

cy.get('[data-cy="save"]').click()
cy.get('[data-cy="saved-message"]').should('be.visible')
cy.get('[data-cy="saved-message"]').should('not.exist')

The final example is appropriate only if the message is expected to appear and then disappear within the test’s timing and app behavior. Otherwise, split the appearance and disappearance behavior into assertions that match the product’s actual state transitions. See Cypress query and retry behavior.

7. Configure selectors generated by Cypress tools

Cypress.ElementSelector configures the priority of attributes used by selector-generating tools such as Cypress Studio and cy.prompt(). The documented default order begins with data-cy, data-test, data-testid, and data-qa, then includes attributes such as name, id, class, and tag.

The selector priority page marks this API as under active development. Check the current Cypress.ElementSelector API before depending on its exact behavior or setting it as a long-term project contract. Generated selectors should still be reviewed for uniqueness and for whether they express the test’s intent.

8. Handle iframes, shadow DOM, duplicates, and other boundaries

  • Duplicate text: Scope with .within(), or pass an element selector to cy.contains(). Do not assume a common label is unique.
  • Substring matches: A string passed to cy.contains() can match longer text. Use an anchored regular expression when the complete text matters.
  • Hidden elements: Text queries may find hidden elements. Assert visibility when users must be able to see the target.
  • Collections: cy.contains() yields one element. Use cy.get() for a collection and assert its count or contents.
  • Iframes: cy.get() does not automatically descend into an iframe document. Use Cypress’s separate iframe guidance and an approach suited to the app and test setup.
  • Shadow DOM: cy.contains() has an includeShadowDom option; its default follows Cypress configuration. Confirm the setting and command behavior for the application’s shadow-root boundaries.
  • Positional CSS extensions: Prefer Cypress chain methods such as .first() and .eq() when they communicate the intended position more clearly than :first or :eq() in a selector.

These boundaries are documented in the cy.get() and cy.contains() API references. For component tests, the same selector principles apply to the rendered component; see Cypress component testing.

9. Troubleshoot selector failures

Symptom Likely cause Fix
cy.get() finds no element The selector is wrong, the app has not rendered the element, or the target is outside the current document boundary. Check the rendered markup and selector spelling. Assert the state that should render the element; use iframe-specific handling when applicable.
The test clicks the wrong matching control The selector is broad, text is duplicated, or cy.contains() chose a preferred ancestor. Scope to a container, add an element selector, or use a unique data-* hook.
The test breaks after a CSS change It targets a styling class or fragile DOM structure. Add a dedicated test attribute and select it with cy.get().
The test breaks after copy changes It uses text as a locator even though wording is not the behavior under test. Use a test hook. Keep text matching when the wording is intentionally under test.
cy.contains() matches more than expected The string is a substring, or several elements contain that text. Use an anchored regular expression for exact text and scope the query or specify the target element type.
An invisible target passes a text query cy.contains() can match hidden elements. Add .should('be.visible') when visibility matters.
A query works in the page but not inside a frame cy.get() does not automatically enter iframe documents. Use an iframe-specific strategy documented by Cypress; do not treat the frame as part of the top-level document.
A shadow-root element is not found The query’s shadow DOM behavior or configuration does not include the relevant root. Check the command’s options and Cypress configuration; set includeShadowDom where supported and appropriate.
A transient disappearance assertion passes immediately The assertion ran before the element appeared. Assert the expected appearance after the action before asserting disappearance, if both transitions are part of the behavior.
A generated selector changes unexpectedly The selector-generation priority or tool behavior differs from assumptions. Review the generated selector and current ElementSelector documentation; prefer an explicit stable hook in critical tests.

10. Keep selector suites reliable and maintainable

  • Agree on one test attribute convention and apply it to interactive controls and important state containers.
  • Make selectors express intent: stable hook for implementation-independent location, text for behavior that depends on wording, and role/name for accessible interaction.
  • Scope repeated components, rows, dialogs, and navigation regions before querying common labels.
  • Prefer retryable queries and assertions over arbitrary delays.
  • Keep hooks unique within their intended scope, and assert collection size where multiplicity matters.
  • Review generated selectors rather than treating generated output as a guarantee of durable intent.

Selector choice has no meaningful cost or performance benchmark established by the Cypress sources cited here. In practice, keep queries specific and avoid adding fixed waits to compensate for an unclear selector or an unasserted application state. Cypress retries queries and assertions within their timeout; a selector that can never match still fails when that timeout expires.

Or skip the browser setup

If you need a rendered reference image of a page while building or debugging a test, ScreenshotNeo is a website screenshot API and MCP server. It does not replace Cypress DOM selectors: Cypress queries the app under test, while ScreenshotNeo returns a rendered page image or PDF.

Make one GET request to capture a URL. See the ScreenshotNeo API documentation for request options.

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://stripe.com \
  -o shot.webp
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)
const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://stripe.com'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const bytes = new Uint8Array(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', bytes));
  • Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off.
  • Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed. Response headers report the page verdict and billing status.
  • An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for AI agents and MCP clients.
  • The free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 screenshots; every feature is available on every plan.

Sign up for ScreenshotNeo to get 1,000 free screenshots a month with no card.

FAQ

Should every Cypress test use a data attribute?

No. Use one when stable location is the goal. Use text or an accessible role and name when those user-facing properties are what the test should verify.

Does cy.contains() require an exact match?

No. String matching can find a substring. Use an anchored regular expression such as /^Save$/ when the complete text should match.

Can I use cy.get() to find an alias?

Yes. Retrieve it with cy.get('@alias'). DOM aliases normally rerun their originating queries unless created as static aliases.

Can Cypress selectors inspect a screenshot?

No. Cypress selectors query DOM elements in the application under test. A screenshot is a rendered image; it is useful as a visual reference but does not expose DOM nodes for Cypress queries.

Sources