ScreenshotNeo

BlogHow-to

How to Use Cypress Selectors to Find Elements

Choose resilient Cypress selectors, scope queries correctly, and troubleshoot retries, text matching, iframes, and shadow DOM.

By the ScreenshotNeo team4 October 20268 min read

Cypress finds elements with queries such as cy.get() and cy.contains(). For a stable test that should survive styling and copy changes, add a dedicated attribute such as data-cy and query it with cy.get('[data-cy="submit"]'). Use cy.contains() when the visible text is itself part of what the test should verify. Scope queries with .within() or .find() when the target belongs inside a particular region.

The locator is a test-design choice: should the test fail if the label changes? If yes, test the text. If no, use a stable test hook. Cypress recommends data-* attributes to isolate selectors from CSS or JavaScript changes. See Cypress best practices for selecting elements.

1. Add a stable test attribute

Put a dedicated test hook on the element in your application markup:

<form data-cy="account-form">
  <label for="email">Email</label>
  <input id="email" name="email" data-cy="email" type="email" />
  <button data-cy="submit" type="submit">Submit</button>
</form>

Then query the hook and assert or interact with the returned element:

cy.get('[data-cy="submit"]')
  .should('be.enabled')
  .click()

The attribute gives the test a locator that does not depend on a styling class or generic tag. Use names that communicate the element’s role in the test, and keep them consistent with the rest of your application. Avoid selecting by a class that exists only to control visual styling when that styling is free to change.

2. Choose the right locator

Locator Use it when Tradeoff
[data-cy="..."] or another dedicated data-* hook The test needs a stable identity independent of styling or incidental text. You add and maintain test attributes in application markup.
cy.contains() The wording itself matters to the behavior or assertion. Copy changes and localization can change the match; it yields at most one element.
findByRole or findByLabelText You want accessibility-oriented queries through Cypress Testing Library. The query alone does not prove full accessibility conformance.
CSS tag, class, or ID The attribute is intentionally part of the behavior, or no better hook is available. Generic tags and styling classes can be brittle; an ID may be coupled to application behavior.

For example, use visible text when the button label is part of the requirement:

cy.contains('button', 'Submit').click()

The button argument constrains candidates to buttons, which can help when the same text appears elsewhere or inside nested markup. cy.contains() yields at most one element. It can also find a hidden element, so assert visibility explicitly when visibility matters:

cy.contains('button', 'Submit')
  .should('be.visible')
  .click()

Text matching is case-sensitive by default. If matching without regard to case is appropriate for the test, pass matchCase: false:

cy.contains('button', 'submit', { matchCase: false })
  .should('be.visible')
  .click()

Think about localization: text varies by locale. Decide whether a test should verify a particular translated label or exercise the underlying control regardless of its copy. Cypress’s guidance also discusses text queries and localization in its introduction to Cypress.

3. Scope a query to the intended region

cy.get() begins searching from the document, unless it runs within an active .within() subject. .find() searches descendants of the current subject. Use the command that matches the scope you intend; a document-wide query can find a duplicate elsewhere on the page.

Use .within() to run multiple queries inside a container:

cy.get('[data-cy="account-form"]').within(() => {
  cy.get('[data-cy="email"]').type('reader@example.test')
  cy.get('[data-cy="submit"]').click()
})

Or chain .find() when you need one descendant:

cy.get('[data-cy="account-form"]')
  .find('[data-cy="email"]')
  .type('reader@example.test')

Outside .within(), a fresh cy.get() starts from the document. Cypress explains the scope and retry behavior in its cy.get() reference.

4. Handle repeated matches and dynamic rendering

When several elements match and the test intentionally targets one by its position, use .first() or .eq(index) to make that intent visible:

cy.get('[data-cy="result-row"]').first().click()
cy.get('[data-cy="result-row"]').eq(2).should('be.visible')

Prefer a unique attribute or a narrower container when position is incidental. A positional locator can silently target a different item if the list order changes.

Cypress queries retry while waiting for elements and chained assertions, subject to the configured command timeout. This supports elements that appear after rendering or asynchronous updates; it does not mean every page boundary is traversed. Keep the query and its assertions chained so Cypress can retry them together:

cy.get('[data-cy="status"]')
  .should('be.visible')
  .and('contain', 'Saved')

For generated selectors from Cypress Studio or cy.prompt(), Cypress.ElementSelector.defaults() can configure priorities. The selector-priority API is described as under active development, so check the documentation for the Cypress release installed in your project before relying on generated-selector settings. See Cypress.ElementSelector.

5. Know the iframe and shadow DOM boundaries

cy.get() does not search inside iframe documents. If a target is inside an iframe, a regular document query will not cross into it. Shadow DOM is also a boundary unless you opt into shadow DOM searching or explicitly traverse the shadow root.

For a shadow DOM target, Cypress documents these approaches:

// Include shadow DOM in this text query when appropriate
cy.contains('button', 'Continue', { includeShadowDom: true })

// Or traverse from a host element explicitly
cy.get('[data-cy="widget-host"]')
  .shadow()
  .find('[data-cy="continue"]')
  .click()

Use includeShadowDom when broad shadow-root searching is intended; use .shadow() when the host gives you a clear scope. The cy.contains() reference documents its matching, scope, and shadow DOM options.

6. Troubleshoot selectors that fail

Symptom Likely cause Fix
cy.get() times out without finding an element Misspelled selector, element not rendered in time, wrong query scope, or element inside a boundary. Check the selector and markup, verify the element appears, scope from the right container, and check for an iframe or shadow root.
A query finds a matching element in the wrong section A document-wide query matched a duplicate. Start from the intended container and use .within() or .find().
cy.contains() does not match capitalization Text matching is case-sensitive by default. Match the expected case or use { matchCase: false } where case should not matter.
cy.contains() passes for a hidden element The query can yield hidden elements. Add .should('be.visible') when visibility is part of the behavior.
A text query selects an unexpected match Text appears in several places, nested markup affects candidates, or an earlier chained contains() narrowed the scope unexpectedly. Constrain by element type, select a container first, and query within it. Avoid chaining multiple contains() calls when the first result can hide the next target.
Query does not find an element in an iframe cy.get() does not descend into iframe documents. Recognize the iframe boundary and use an approach designed for the iframe case; a document-level selector will not cross it.
Query does not find a shadow-root element The query did not include or traverse the shadow root. Use includeShadowDom where supported or traverse the host with .shadow().
Generated selector changes after a Cypress upgrade Generated selector priorities are version-sensitive and under active development. Check the API docs for the installed release; for critical tests, use an explicit stable test hook.

7. Performance, reliability, and maintenance

  • Prefer specific hooks. A dedicated test attribute communicates intent and avoids accidental matches on generic tags or presentation classes.
  • Scope repeated queries. Narrowing to a form, dialog, or row makes the intended match clearer and avoids selecting a duplicate elsewhere.
  • Let Cypress retry queries. Queries and chained assertions retry up to their configured timeout; increasing timeouts does not correct a selector that points at the wrong scope or crosses an unsupported boundary.
  • Use text strategically. Text selectors make the test sensitive to visible content, which is useful when content is the behavior. For a behavior test independent of copy, a stable hook is usually less fragile.
  • Choose semantics deliberately. Accessibility-oriented queries can help locate controls by role or label, but passing a query is not a complete accessibility audit.

Selector quality affects how often test maintenance is needed and how clearly failures identify the behavior under test. There is no numeric performance claim here; use the simplest specific query that expresses the intended behavior.

8. Or skip the browser setup

If your next step is capturing the page for review or documentation, ScreenshotNeo can return a screenshot from one GET request. See the ScreenshotNeo API docs 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}`);
await Bun.write('shot.webp', res);

ScreenshotNeo accepts cookie or consent banners like a visitor and removes 60+ known consent platforms, newsletter popups, and chat widgets before the capture; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers say the page verdict and whether the request was billed. Its MCP server gives AI agents such as Claude and Cursor the take_screenshot, get_page_info, and capture_pdf tools. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots. Learn about ScreenshotNeo, then sign up for 1,000 free screenshots a month with no card.

9. FAQ

Should I use data-cy or data-testid?

Either can be a dedicated test hook. Cypress’s examples use data-cy; choose a project convention and use it consistently.

Does a successful role query prove the page is accessible?

No. Accessibility-oriented queries help locate elements through semantics, but a query by itself is not a complete accessibility evaluation.

When should I test the button’s text?

Use a text query when the label is part of the expected behavior and changing it should fail the test. Otherwise, locate the control through a stable test hook.

Does increasing the command timeout fix a selector that returns the wrong element?

No. A timeout helps only when the intended element needs more time to appear; fix the selector or scope when it targets the wrong match.