Resource library

QA How-To

Cypress data-cy selectors: Examples

Copy each cypress data-cy selectors example for forms, tables, dialogs, lists, React components, custom commands, network waits, shadow DOM, and migrations.

23 min read | 2,459 words

TL;DR

The standard cypress data-cy selectors example is cy.get('[data-cy="save-profile"]').click(). Stronger examples also scope repeated components, assert outcomes, and keep markup hooks named by business purpose.

Key Takeaways

  • Pair application markup and test code so every data-cy value has an intentional consumer.
  • Use exact selectors for single controls and scope repeated values to the correct business record.
  • Place hooks on actionable elements, not decorative descendants or volatile layout wrappers.
  • Wait for observable network or UI state before querying hooks that render asynchronously.
  • Keep custom getByCy helpers transparent, typed, and free of hidden first-match fallbacks.
  • Use component tests to verify forwarding and end-to-end tests to verify business outcomes.
  • Migrate brittle selectors by intent, and use text or semantic locators where they better express the requirement.

The simplest cypress data-cy selectors example is cy.get('[data-cy="save-profile"]').click(), but production suites need more. Forms need validation scopes, tables repeat row hooks, dialogs can coexist in the DOM, component libraries must forward attributes, and asynchronous screens need observable readiness rather than fixed waits.

This recipe collection pairs application markup with Cypress TypeScript specs. Replace the illustrative routes, hooks, and fixture data with your product contracts. Every Cypress method shown is a current API, and no example depends on a fictional built-in selector command.

TL;DR

Recipe Selector shape Outcome to assert
Login form Exact field and submit hooks Authenticated route and identity
Validation Form scope plus error hooks Message, focus, and disabled state
Table row Repeated row plus invoice number Correct row action and result
Dialog Visible dialog scope plus child hook Dialog closes and page updates
Async results Intercept alias plus result hook Expected records render
Shadow component Host, shadow(), internal hook Public component behavior changes
Custom command Typed exact-value helper Same normal Cypress query behavior

Use these examples as patterns, not as permission to add hooks to every HTML node.

1. Basic Cypress data-cy Selectors Example

Add the hook to the actual interactive element. The test locates it by exact value, confirms actionability, performs the action, and verifies a separate outcome.

<button
  type="button"
  data-cy="cart-checkout"
>
  Continue to checkout
</button>
<div role="status" data-cy="checkout-status"></div>
cy.get('[data-cy="cart-checkout"]')
  .should('be.visible')
  .and('be.enabled')
  .click()

cy.location('pathname').should('eq', '/checkout')
cy.get('[data-cy="checkout-status"]')
  .should('have.text', 'Ready for payment')

The selector says which business action the test wants. It does not depend on the button's tag hierarchy, color, utility classes, or position. The assertions still prove user-observable behavior through the route and status.

Avoid attaching data-cy="cart-checkout" only to a nested icon. Cypress would click the icon's center rather than treating the button as the interaction contract, and the icon might disappear during a redesign. Put the hook on the button.

If the visible label is the requirement, add a text assertion or use cy.contains('button', 'Continue to checkout'). A hook is an identity mechanism, not a substitute for all semantic validation.

2. Login and Validation Form Example

Use a form root for scoping and distinct hooks for controls whose stable identity matters. Preserve real labels and native form semantics. The selectors make the workflow durable, while error assertions prove validation.

<form data-cy="login-form">
  <label for="login-email">Email</label>
  <input id="login-email" type="email" data-cy="login-email" />
  <p data-cy="login-email-error" role="alert"></p>

  <label for="login-password">Password</label>
  <input id="login-password" type="password" data-cy="login-password" />
  <button type="submit" data-cy="login-submit">Sign in</button>
</form>
describe('login validation', () => {
  beforeEach(() => {
    cy.visit('/login')
  })

  it('requires a valid email', () => {
    cy.get('[data-cy="login-form"]').within(() => {
      cy.get('[data-cy="login-email"]').type('not-an-email')
      cy.get('[data-cy="login-password"]').type('safe-fixture-password', {
        log: false,
      })
      cy.get('[data-cy="login-submit"]').click()
      cy.get('[data-cy="login-email-error"]')
        .should('be.visible')
        .and('have.text', 'Enter a valid email address')
    })

    cy.location('pathname').should('eq', '/login')
  })
})

within() prevents a duplicate login control elsewhere, such as a header modal, from matching. It does not mean every child must have a hook. Native fields can also be found through a supported label-oriented locator library if that is your team standard.

Do not put real credentials in test source or hook values. log: false reduces command log exposure for typed text but is not a complete secret policy. Use protected configuration and test accounts.

3. Dynamic Table Row Example

Tables repeat structure, so repeat a row-level hook. Narrow the row with a stable invoice number rendered from controlled test data, then operate on the child menu within that row.

<table data-cy="invoice-table">
  <tbody>
    <tr data-cy="invoice-row">
      <td data-cy="invoice-number">INV-1048</td>
      <td data-cy="invoice-status">Sent</td>
      <td><button data-cy="invoice-menu">Actions</button></td>
    </tr>
    <tr data-cy="invoice-row">
      <td data-cy="invoice-number">INV-1049</td>
      <td data-cy="invoice-status">Draft</td>
      <td><button data-cy="invoice-menu">Actions</button></td>
    </tr>
  </tbody>
</table>
cy.contains('[data-cy="invoice-row"]', 'INV-1049')
  .as('draftInvoice')

cy.get('@draftInvoice').within(() => {
  cy.get('[data-cy="invoice-status"]').should('have.text', 'Draft')
  cy.get('[data-cy="invoice-menu"]').click()
})

cy.get('[data-cy="invoice-actions"]')
  .should('be.visible')
  .contains('button', 'Send invoice')
  .click()

cy.contains('[data-cy="invoice-row"]', 'INV-1049')
  .find('[data-cy="invoice-status"]')
  .should('have.text', 'Sent')

The final query re-locates the row after the action. A framework may replace the original row during the status update, so re-querying is safer than preserving a jQuery node alias through mutation.

Avoid hooks such as invoice-row-0 or invoice-row-1049 by default. The first depends on sort order. The second couples markup to generated data and may expose identifiers. A repeated row plus controlled business content is usually clearer.

4. Dialog and Drawer Scoping Example

Applications often keep hidden dialogs mounted or render several portals under body. Start from the visible dialog contract, then find child controls within it. This prevents a hidden duplicate action from matching.

cy.get('[data-cy="delete-project"]').click()

cy.get('[data-cy="confirm-delete-dialog"]')
  .should('be.visible')
  .within(() => {
    cy.get('[data-cy="confirm-project-name"]')
      .should('have.text', 'Mobile Checkout')
    cy.get('[data-cy="confirm-delete"]').click()
  })

cy.get('[data-cy="confirm-delete-dialog"]').should('not.exist')
cy.get('[data-cy="project-list"]')
  .should('not.contain.text', 'Mobile Checkout')

If the UI hides the dialog instead of removing it, use should('not.be.visible') based on the product contract. Do not arbitrarily switch between nonexistence and invisibility. They represent different DOM outcomes.

A dialog's buttons can have short scoped hooks such as confirm and cancel if your naming policy makes component scope explicit. Globally meaningful values such as confirm-delete are easier to search across source. Choose one approach and keep it consistent.

Use roles and accessible names as additional assertions. A dialog hook can find the correct portal while should('have.attr', 'role', 'dialog') and an accessible name check validate semantics.

5. Network-Driven Search Results Example

A stable hook cannot solve a data-loading race. Register an intercept before the action, wait for the matching request, assert the response contract, then query rendered result hooks. The alias establishes readiness without sleeping.

cy.intercept('GET', '/api/search*').as('search')
cy.visit('/catalog')

cy.get('[data-cy="catalog-search"]')
  .type('mechanical keyboard{enter}')

cy.wait('@search').then(({ request, response }) => {
  expect(request.query.q).to.eq('mechanical keyboard')
  expect(response?.statusCode).to.eq(200)
})

cy.get('[data-cy="search-results"]')
  .should('be.visible')
  .find('[data-cy="product-card"]')
  .should('have.length.at.least', 1)

cy.contains('[data-cy="product-card"]', 'Compact Mechanical Keyboard')
  .find('[data-cy="add-to-cart"]')
  .click()

cy.get('[data-cy="cart-count"]').should('have.text', '1')

The network wait confirms the defining request completed. The DOM assertions confirm the application rendered it. Both layers matter. Waiting only on the request can still race with rendering, while waiting only on a broad container may accept stale results.

Register the intercept before typing, or a fast request may escape it. Match the route narrowly enough that an unrelated background search does not satisfy the alias. See Cypress intercept examples for request matching and stubbing patterns.

6. Cypress data-cy Selectors Example for React Components

A reusable React component should accept the standard attribute and forward it to the actionable DOM element. This keeps the component API compatible with ordinary JSX attributes and allows each screen to provide contextual identity.

type MenuButtonProps = {
  label: string
  onOpen: () => void
  'data-cy'?: string
}

export function MenuButton({
  label,
  onOpen,
  'data-cy': dataCy,
}: MenuButtonProps) {
  return (
    <button
      type="button"
      aria-label={label}
      data-cy={dataCy}
      onClick={onOpen}
    >
      <span aria-hidden="true">...</span>
    </button>
  )
}

export function ProjectHeader() {
  return (
    <header data-cy="project-header">
      <h1>Mobile Checkout</h1>
      <MenuButton
        label="Project actions"
        data-cy="project-menu"
        onOpen={() => {}}
      />
    </header>
  )
}

A Cypress component test can verify forwarding and interaction:

import { mount } from 'cypress/react'
import { MenuButton } from './MenuButton'

it('forwards the hook and handles activation', () => {
  const onOpen = cy.stub().as('onOpen')

  mount(
    <MenuButton
      label="Project actions"
      data-cy="project-menu"
      onOpen={onOpen}
    />,
  )

  cy.get('[data-cy="project-menu"]').click()
  cy.get('@onOpen').should('have.been.calledOnce')
})

The hook is additive. The component remains a native button with an accessible label. Do not make the test-only attribute the only meaningful contract.

7. Typed getByCy Custom Command Example

If direct selector strings dominate the suite, a small helper can reduce punctuation and centralize the attribute name. Declare it for TypeScript and return cy.get() without altering its semantics.

// cypress/support/commands.ts
declare global {
  namespace Cypress {
    interface Chainable {
      getByCy(
        value: string,
        options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
      ): Chainable<JQuery<HTMLElement>>
    }
  }
}

Cypress.Commands.add('getByCy', (value, options = {}) => {
  return cy.get(`[data-cy="${value}"]`, options)
})

export {}
// cypress/e2e/profile.cy.ts
cy.getByCy('profile-name').clear().type('Riley Chen')
cy.getByCy('profile-save').click()
cy.getByCy('save-status').should('have.text', 'Saved')

Keep hook values constrained to your naming convention. A general CSS escaping utility is required if arbitrary runtime strings can enter the selector, but selector names should normally be code-owned literals.

Do not have the helper call first(), fall back to text, increase all timeouts, or automatically force clicks. Those choices conceal ambiguous contracts and application problems. A reader should be able to translate getByCy('profile-save') directly into one exact CSS query.

The related how to use Cypress data-cy selectors guide covers naming and governance decisions behind this helper.

8. Shadow DOM Example

For a component with an open shadow root that your team owns, locate the host, enter its shadow root, then query an internal data-cy hook. Assert a public result outside the component when possible.

<notification-settings data-cy="notification-settings"></notification-settings>
<div data-cy="settings-status" role="status"></div>

The component's open shadow root renders a checkbox with data-cy="weekly-summary". The Cypress test is:

cy.get('[data-cy="notification-settings"]')
  .shadow()
  .find('[data-cy="weekly-summary"]')
  .check()
  .should('be.checked')

cy.get('[data-cy="settings-status"]')
  .should('have.text', 'Preferences updated')

Alternatively, Cypress queries support the project and command-level includeShadowDom option. Explicit shadow() makes the component boundary visible in this example. Choose a consistent approach based on how often shadow components appear.

Do not reach into closed roots or third-party internals unless the component's supported testing surface requires it. Prefer interaction through public controls, events, and visible behavior. Internal hooks are most maintainable when your product team owns both the component and the test contract.

9. Feature Flag and Conditional Rendering Example

Make the flag deterministic before visiting, then assert the expected hook. Do not inspect body once and conditionally skip the scenario if the target is missing. That can turn a rendering bug into a passing test.

beforeEach(() => {
  cy.request('PUT', '/test-support/features/checkout-v2', {
    enabled: true,
  })
})

it('uses the new checkout flow', () => {
  cy.visit('/cart')
  cy.get('[data-cy="checkout-v2"]')
    .should('be.visible')
    .within(() => {
      cy.get('[data-cy="delivery-method"]').select('Express')
      cy.get('[data-cy="continue-to-payment"]').click()
    })

  cy.location('pathname').should('eq', '/checkout/payment')
})

Write another test with the flag disabled if the legacy branch remains supported. Each branch gets an explicit name and precondition. When the feature graduates, delete the obsolete setup, hooks, and tests together.

The hook can describe the feature boundary, such as checkout-v2, while it is genuinely a separate experimental component. Avoid permanently versioning every hook. Once the old implementation is removed, rename only if the version no longer communicates useful domain meaning and coordinate the contract change with test updates.

10. Migrate a Brittle Selector Example

Migrate by identifying the business target, not by mechanically replacing every CSS string. This old selector depends on generated classes, container depth, and position:

// Before
cy.get('.css-18x7k > div:nth-child(2) button.btn-primary').click()

Add a product-owned hook to the action:

<button class="btn-primary" data-cy="shipping-continue">
  Continue
</button>
// After
cy.get('[data-cy="shipping-continue"]').click()
cy.get('[data-cy="payment-step"]').should('be.visible')

Review whether the label is important. If the scenario verifies wording, use or assert Continue. Review whether the new hook belongs on the button and whether its name will survive a redesigned layout. Search for all consumers of the old selector before removing supporting markup.

Migrate high-churn and low-clarity selectors first. Do not add hooks to every heading, paragraph, and input in one mass change. Many elements are better located through text, labels, or component scope. Track failures after migration to verify that instability was truly selector-related rather than caused by uncontrolled data or timing.

For broader cleanup, use the Cypress flaky test debugging guide to separate locator problems from state, network, and test isolation issues.

11. Pagination and Infinite List Example

A repeated row hook only locates records currently rendered. For pagination, use the product controls to reach the expected page, then scope the target row. For an infinite list, trigger the supported scroll behavior and assert the loading transition before searching again.

cy.intercept('GET', '/api/audit-events*').as('auditEvents')
cy.visit('/audit')
cy.wait('@auditEvents')

cy.get('[data-cy="audit-list"]')
  .find('[data-cy="audit-row"]')
  .should('have.length.at.least', 1)

cy.get('[data-cy="load-more"]')
  .should('be.visible')
  .and('be.enabled')
  .click()

cy.wait('@auditEvents').then(({ request, response }) => {
  expect(request.query).to.have.property('cursor')
  expect(response?.statusCode).to.eq(200)
})

cy.get('[data-cy="audit-row"]')
  .should('have.length.greaterThan', 1)

cy.contains('[data-cy="audit-row"]', 'Project archived')
  .find('[data-cy="audit-details"]')
  .click()

cy.get('[data-cy="audit-drawer"]')
  .should('be.visible')
  .and('contain.text', 'Project archived')

Register the alias once when the same route serves the first and next pages. Cypress waits for successive matching requests on successive cy.wait() calls. Assert the cursor so an unrelated refresh cannot satisfy the second wait.

Do not create audit-row-37 merely because the record appears later in the collection. The repeated component hook remains correct. Navigation and server state determine whether that component exists in the DOM.

12. Localization and Accessible Name Example

Use a hook when translated text should not control element identity, then assert the localized copy when localization is itself part of the test. This separates stable action identity from language expectations.

cy.visit('/settings?locale=fr-CA')

cy.get('[data-cy="language-form"]').within(() => {
  cy.get('[data-cy="language-select"]')
    .should('have.value', 'fr-CA')
  cy.get('[data-cy="language-save"]')
    .should('have.text', 'Enregistrer')
    .click()
})

cy.get('[data-cy="save-status"]')
  .should('have.attr', 'role', 'status')
  .and('have.text', 'Préférences enregistrées')

The hook allows the workflow to use the same identity in every locale. The text assertions still catch a missing translation, fallback language, or incorrect resource. For a general settings workflow that does not verify translation, assert only stable outcomes and keep copy checks in localization-focused coverage.

An icon-only control should retain an accessible name even when selected by hook:

cy.get('[data-cy="close-settings"]')
  .should('have.attr', 'aria-label', 'Fermer les paramètres')
  .click()

cy.get('[data-cy="settings-dialog"]').should('not.exist')

Do not derive the hook from translated text. Values such as data-cy="enregistrer" couple the automation contract to locale and make cross-language maintenance harder. Keep hook values language-neutral and keep required translations in assertions or dedicated localization data.

This combined pattern also improves defect classification. A missing hook is an integration contract issue, incorrect visible French is a localization issue, and a missing accessible label is a semantic issue. One locator strategy does not need to carry all three responsibilities.

13. Loading, Disabled, and Replaced Control Example

Use one stable hook across a control's state transitions when the same business action remains. Assert disabled and loading semantics, wait for the defining request, then re-query because the framework may replace the button after completion.

cy.intercept('POST', '/api/exports').as('createExport')
cy.visit('/reports/monthly')

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

cy.get('[data-cy="create-export"]')
  .should('be.disabled')
  .and('have.attr', 'aria-busy', 'true')

cy.wait('@createExport').then(({ request, response }) => {
  expect(request.body).to.include({ format: 'csv' })
  expect(response?.statusCode).to.eq(202)
})

cy.get('[data-cy="create-export"]')
  .should('be.enabled')
  .and('have.attr', 'aria-busy', 'false')

cy.get('[data-cy="export-status"]')
  .should('have.attr', 'role', 'status')
  .and('contain.text', 'Export queued')

The repeated queries are intentional. React, Vue, or another renderer may replace the original node while toggling the loading state. Cypress queries locate the current element, while the stable create-export value preserves business identity.

Do not rename the hook to create-export-loading during the request and back afterward. State belongs in disabled, aria-busy, visible text, or another assertion contract. Changing selector identity with state makes tests needlessly conditional.

If the action can finish too quickly to observe loading in an end-to-end environment, control the response with cy.intercept() in a focused loading-state test. Keep a separate integration test for the real backend response. Never add a production delay or fixed test sleep merely to catch a transient frame.

Interview Questions and Answers

Q: Show the standard syntax for a data-cy selector.

Use cy.get('[data-cy="checkout-submit"]'). It is a standard CSS attribute selector passed to the built-in cy.get() query. I normally use an exact quoted value.

Q: How would you act on one row in a repeated table?

I give each row the same component hook, narrow to the row with a controlled business identifier, and query the action inside that row. I re-query the row after an action that may replace it. I avoid array indexes unless order is under test.

Q: How do you handle a data-cy element that appears after an API response?

I register a narrow intercept before the triggering action, wait for its alias, and then assert the rendered hook. I still verify the UI state because network completion does not guarantee render completion. I do not use a fixed wait.

Q: Where should data-cy go in a custom button component?

It should be forwarded to the actual native button that receives the action. The component should keep its role, accessible name, state, and keyboard behavior. A decorative icon or wrapper is the wrong interaction target.

Q: What should a getByCy custom command avoid?

It should avoid hidden first() calls, fallback selectors, forced actions, and global timeout changes. These can make a test pass against the wrong target. The helper should remain a transparent exact-value query.

Q: How would you test both sides of conditional rendering?

I control the feature or account state before the visit and write a named test for each supported branch. Each test asserts its expected hook and outcome directly. I do not let an early DOM snapshot choose whether the assertion runs.

Q: How do you decide which brittle selectors to migrate first?

I prioritize selectors with frequent failures, generated classes, positional paths, or unclear intent. For each, I choose the strongest contract, which may be a data hook, label, role, or required text. I update product markup and consuming tests in the same review.

Common Mistakes

  • Adding the hook to an icon or wrapper instead of the element users interact with.
  • Using one global selector when the target should be scoped to a visible form, row, or dialog.
  • Reusing a DOM element after an action that re-renders or removes it.
  • Encoding list indexes or random database identifiers into hook values.
  • Waiting a fixed number of milliseconds for network-driven elements.
  • Registering cy.intercept() after the request has already started.
  • Giving custom selector helpers hidden fallback or first-match behavior.
  • Treating a found hook as proof that the workflow succeeded.
  • Allowing a React or Vue wrapper to drop the attribute before it reaches the DOM.
  • Branching on a possibly incomplete DOM snapshot and silently skipping coverage.
  • Replacing every locator with data-cy without considering user-facing semantics.

Conclusion

A production-quality cypress data-cy selectors example pairs a stable application hook with precise scope and a meaningful outcome. Use exact values for individual controls, repeat component hooks for lists, re-query after renders, and coordinate asynchronous state through intercepts or observable UI conditions.

Pick the recipe closest to your application, add only the hooks the workflow needs, and review the markup and test together. The selector should survive incidental refactoring while the assertions continue to fail when user behavior actually breaks.

Interview Questions and Answers

Write a basic data-cy Cypress query.

I would write cy.get('[data-cy="checkout-submit"]').should('be.enabled').click(). Then I would assert the resulting route, confirmation, or domain state. The hook finds the action, while the later assertion proves behavior.

How do you avoid positional selectors in tables?

I repeat a row-level hook, locate the row containing a stable fixture-backed business value, and query the child action in that scope. I use position only when sorting or order is explicitly being tested.

How should network timing interact with data-cy queries?

The hook handles identity, not readiness. I register an intercept before the action, wait on the defining request, and then assert the rendered hook and content. This covers transport and UI rendering separately.

How would you expose data-cy from a component library?

I accept the standard attribute in the component props and forward it to the meaningful native element. I retain accessible semantics and test event behavior in a component test. I avoid attaching the same hook to wrapper and child.

What makes a custom selector command maintainable?

It has a TypeScript declaration, returns standard cy.get behavior, accepts normal query options, and constructs one exact selector. It does not silently choose the first match, change timeouts, or fall back to unrelated locators.

How do you test feature-flagged markup?

I set a deterministic flag state before visiting and write separate tests for each supported branch. Each test directly asserts its expected hook and user outcome. I remove obsolete hooks and tests when the flag graduates.

What is your process for migrating a brittle selector?

I identify the business target and decide whether a hook, role, label, or text is the strongest contract. I add or preserve product semantics, update all consumers, and assert the resulting behavior. I monitor whether remaining failures are really timing or data issues.

Frequently Asked Questions

What is a simple Cypress data-cy example?

Add data-cy="save-profile" to the application button, then call cy.get('[data-cy="save-profile"]').click(). Follow the action with an assertion on the saved result.

How do I use data-cy inside within?

Locate the form, row, or dialog root, call .within(), and run child cy.get() queries in the callback. This limits matching to the selected component scope.

How do I select a dynamic table row in Cypress?

Give every row the same data-cy value, narrow to a row using a controlled invoice number or other business content, and find the child action inside it. Avoid row indexes when order is not the requirement.

Can data-cy work inside shadow DOM?

Yes, for an open shadow root. Locate the host, call .shadow(), and find the internal data-cy target, or use the supported includeShadowDom option according to project policy.

How do I add data-cy to a React component?

Accept the 'data-cy' prop and forward it to the meaningful native DOM element. Confirm with a component test that the attribute is rendered on the intended action target.

Should getByCy call first()?

No. Automatically choosing the first match hides duplicate or under-scoped contracts. Scope the test or fix the markup so the intended target is explicit.

How do I wait for a data-cy element after a request?

Register cy.intercept() before the triggering action, wait for the alias, then use a retryable cy.get() and assertion on the rendered element. Avoid fixed sleeps.

Related Guides