ScreenshotNeo

BlogHow-to

How to Find HTML Elements with Cypress Locators

Choose reliable Cypress locators with cy.get(), cy.contains(), and .find(). Learn how to scope queries, handle retries, and troubleshoot common selector failures.

By the ScreenshotNeo team4 October 20267 min read

Use cy.get() with a dedicated test attribute such as [data-cy="submit"] when you need a stable element identity. Use cy.contains() when the visible text is part of what the test should verify, and use .find() to search descendants of a selected parent. Use .within() to scope several commands to one region.

Cypress queries are queued and retried; they do not return DOM elements synchronously like ordinary jQuery calls. The examples below assume Cypress is installed and the app under test is running.

1. Choose a locator that matches the test’s intent

Locator Use it when Trade-off
cy.get('[data-cy="..."]') The element needs a stable identity even if its text or styling changes. Add and maintain a test-specific attribute in the markup.
cy.contains('...') The visible copy itself is behavior the test should catch if changed. Text can change with copy edits or localization; Cypress may yield a preferred interactive ancestor.
Semantic attributes or CSS structure The tested structure or attribute is meaningful and stable. Classes tied to styling and broad tags are often fragile or ambiguous.
Testing Library query, such as findByRole You want role- or label-oriented queries in Cypress. Requires Cypress Testing Library. A locator alone is not a full accessibility audit.

Cypress recommends dedicated data-* attributes because they are isolated from CSS and JavaScript changes. A useful decision is: should the test fail if the element’s text changes? If not, prefer a stable test attribute. If the copy is the behavior under test, use text. No locator style by itself makes a test an accessibility audit.

2. Use cy.get() for a stable selector

cy.get(selector) searches from the current Cypress root, normally the document. Put a test attribute on the application element:

<form data-cy="login-form">
  <label for="email">Email</label>
  <input id="email" data-cy="email" type="email">
  <button data-cy="submit" type="submit">Sign in</button>
</form>

Then query and interact with it from a Cypress spec:

describe('login', () => {
  it('submits the login form', () => {
    cy.visit('/login')
    cy.get('[data-cy="email"]').type('reader@example.com')
    cy.get('[data-cy="submit"]').click()
    cy.get('[data-cy="login-status"]').should('be.visible')
  })
})

Use a selector that identifies the intended element uniquely. Avoid using a broad tag or selector such as div, section, or * when a precise attribute is available: broad queries can match many nodes and add unnecessary browser and Cypress work.

3. Use cy.contains() when visible text matters

cy.contains() matches a string, number, or regular expression. It is case-sensitive by default and yields at most one element. Cypress may prefer an interactive element such as a button, link, label, or submit input over a nested text node.

// Click the button whose user-facing label is part of the behavior.
cy.contains('Submit').click()

// Constrain the candidate elements to buttons.
cy.contains('button', 'Submit').click()

// Match without regard to case.
cy.contains('submit', { matchCase: false }).click()

// Match a pattern.
cy.contains(/^Order #\\d+$/).should('be.visible')

Use text queries when a label or message change should make the test fail. For localized applications, decide whether the test should follow the actual localized label or stay language-independent with a test attribute. Do not use cy.contains() to check that a collection has multiple matches; it returns no more than one element.

4. Scope a query with .find() or .within()

.find(selector) searches descendants of the current subject; it does not include the subject itself. It can search descendants at any depth. For a single scoped query:

cy.get('[data-cy="checkout"]')
  .find('[data-cy="confirm"]')
  .click()

Use a leading child combinator when only direct children should match:

cy.get('[data-cy="cart"]')
  .find('> li')
  .should('have.length', 3)

Use .within() when multiple commands should share a selected region:

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

Within the callback, Cypress queries are scoped to that region. This can make repeated queries clearer and avoid accidentally matching a similarly named control elsewhere on the page.

5. Handle retries, timeouts, and application state

Cypress retries queries and chained assertions until they pass or the command times out. For example, cy.get('[data-cy="result"]').should('be.visible') waits for the result and visibility assertion. A timeout is a signal to check the selector, scope, and page state before increasing a wait.

// A command-specific timeout in milliseconds.
cy.get('[data-cy="late-result"]', { timeout: 10000 })
  .should('be.visible')

The timeout can also be configured with Cypress’s defaultCommandTimeout setting. Use a longer timeout only when the application genuinely needs more time; larger defaults can make every failing query take longer to report. Wait for the application condition the test cares about rather than adding arbitrary fixed delays.

6. Work with shadow DOM and iframe boundaries

cy.get() does not search inside an iframe. Shadow DOM is also a boundary by default. For a shadow root, enter it with .shadow() and then query inside:

cy.get('my-widget')
  .shadow()
  .find('[data-cy="save"]')
  .click()

Alternatively, enable shadow-DOM inclusion for a .find() query:

cy.get('my-widget')
  .find('[data-cy="save"]', { includeShadowDom: true })
  .click()

The includeShadowDom option can also be set in Cypress configuration. Prefer explicit traversal when it helps make the component boundary clear. An iframe is a separate document; ordinary cy.get() selectors do not cross into it.

7. Troubleshoot locator failures

Symptom Likely cause Fix
cy.get() times out The selector does not match rendered markup, the page is in the wrong state, or the query starts from the wrong scope. Inspect the actual DOM and selector spelling; confirm navigation or rendering completed; check whether a .within() scope is active.
The wrong element is clicked The selector matches multiple nodes or text matching yields a preferred interactive ancestor. Use a unique test attribute or constrain cy.contains() with an element selector, then assert the target’s state.
.find() returns nothing The target is not a descendant, the current subject is wrong, or the target is inside a shadow root. Check the selected parent; use a root query if it is elsewhere; enter the shadow root with .shadow().
Text locator breaks in another locale The displayed copy differs by language. Use a stable test attribute if locale is not under test; otherwise assert the expected localized text deliberately.
Element is visible in the page but not found It may be inside an iframe or shadow root, which ordinary queries do not cross. Handle the component boundary explicitly; iframe content needs a separate approach and is not searched by cy.get().
A larger timeout does not solve the failure The selector or application state is incorrect, rather than merely slow. Verify the selector against rendered HTML and wait for the real state transition before adjusting timeouts.

8. Performance, reliability, and maintenance

  • Prefer precise selectors. Queries like * can make the browser and Cypress process many candidates.
  • Use stable identity. A dedicated data-* attribute is less coupled to styling than a CSS class that exists for visual presentation.
  • Keep text assertions intentional. Text locators are useful when copy is behavior; they are more exposed to copy edits and localization.
  • Scope repeated work. .within() expresses which region owns a group of controls.
  • Let Cypress retry queries. Prefer a query plus an assertion over synchronous DOM assumptions or arbitrary sleep delays.
  • Expect boundary behavior. Shadow roots and iframes require explicit handling; a document-level query cannot see through them.

Locator costs are generally about clarity, retry work, and test maintenance rather than a separate Cypress charge. A unique selector reduces ambiguity, while a stable attribute helps tests survive unrelated UI restyling.

9. Capture a page to inspect its rendered HTML visually

When a locator fails, inspect the page state in the test runner and compare the selector with the rendered page. A screenshot can help diagnose layout-dependent issues, though it does not replace checking the DOM or selector scope.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. Its one-call API can capture a page as an image; see the API documentation.

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}`);
# Python example above uses requests; install it with:
# python -m pip install requests

ScreenshotNeo accepts cookie and consent banners like a visitor, then removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for AI agents. The free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000 screenshots.

Sign up free for 1,000 screenshots a month, with no card required.

10. Frequently asked questions

Can I use a locator by element ID?

Yes. CSS selectors such as #email work with cy.get(). Use an ID when it is stable and identifies the intended target; a test-specific data attribute makes that intent explicit.

Does cy.contains() return every matching element?

No. It yields at most one match. Use a collection query such as cy.get() with an appropriate selector when you need to assert a count.

Does Cypress locator syntax return an element immediately?

No. Cypress commands are queued and retried. Chain Cypress commands and assertions instead of treating the result as a synchronous DOM value.

Do data attributes replace accessibility testing?

No. They provide stable test identity. Use role- and label-oriented checks where appropriate, and test accessibility separately.

Sources