QA How-To
How to Use Cypress data-cy selectors (2026)
Learn cypress data-cy selectors with naming rules, CSS syntax, scoping, dynamic lists, component patterns, custom commands, debugging, and interview answers.
22 min read | 2,938 words
TL;DR
Add a stable attribute such as data-cy="checkout-submit" to the application element, then query it with cy.get('[data-cy="checkout-submit"]'). Use semantic, kebab-case names and scope repeated components before choosing a child.
Key Takeaways
- Add data-cy attributes as intentional test contracts, then query them with standard CSS attribute selectors.
- Name hooks by stable business purpose, not CSS, layout, framework component names, or indexes.
- Prefer roles, labels, or visible text when user-facing semantics are the behavior under test.
- Scope repeated hooks to a meaningful row, card, form, or dialog before selecting child actions.
- Keep data-cy values unique within their intended scope and free of secrets or personal information.
- Use a small typed custom query helper only when it improves consistency without hiding Cypress behavior.
- Treat selector changes as reviewed contract changes and diagnose rendering or actionability before adding waits.
Cypress data-cy selectors are explicit attributes that give tests a stable way to locate application elements. Add data-cy="business-purpose" to the rendered HTML, then query it with the standard CSS attribute syntax cy.get('[data-cy="business-purpose"]'). Because the hook is separate from styling and JavaScript behavior, visual refactoring is less likely to break the test.
A test hook is still a contract that needs design. Poorly named, duplicated, or overused attributes can make a suite stable but unreadable, while inaccessible UI can remain undetected behind test-only hooks. This guide explains naming, scoping, component integration, custom commands, dynamic content, debugging, and the choice between data-cy and user-facing locators.
TL;DR
| Situation | Preferred locator | Reason |
|---|---|---|
| Stable automation-only action | [data-cy="checkout-submit"] |
Decoupled from CSS and copy |
| Button name is part of the requirement | cy.contains('button', 'Place order') |
Fails when required visible copy changes |
| Labeled form control | Label-aware plugin or stable data-cy |
Choose based on accessibility coverage and dependencies |
| Repeated cards | Parent data-cy plus meaningful filter |
Avoid position-based selectors |
| Styling behavior itself | Class assertion after stable lookup | Use the hook to locate, class to verify |
| Generated or unstable class | data-cy |
Do not couple identity to build output |
A basic application and test pair is:
<button type="submit" data-cy="checkout-submit">Place order</button>
cy.get('[data-cy="checkout-submit"]').click()
cy.get('[data-cy="order-confirmation"]').should('be.visible')
1. What Cypress data-cy Selectors Are
data-cy is a custom HTML data attribute. HTML permits attributes beginning with data-, and CSS attribute selectors can query them. Cypress does not require this exact name or provide a special getByDataCy() command by default. The conventional query is simply cy.get() with a CSS selector.
<form data-cy="login-form">
<label for="email">Email</label>
<input id="email" name="email" data-cy="login-email" />
<button type="submit" data-cy="login-submit">Sign in</button>
</form>
cy.get('[data-cy="login-email"]').type('qa@example.test')
cy.get('[data-cy="login-submit"]').click()
The attribute's main benefit is ownership. Product code declares a stable automation surface, and test code consumes it. Changing a Tailwind class, DOM nesting, or button styling need not change the selector. The hook also communicates that removing or renaming the node can affect automated coverage.
Data attributes do not create global uniqueness rules. Browsers allow duplicate values, and cy.get() can yield several elements. Your team must define whether a value is unique on a page, unique within a component, or intentionally repeated for list items. That scope should be clear from the name and test structure.
A hook identifies an element, but it does not assert usability, text, accessibility, or business outcome. Locate with data-cy when appropriate, then assert the behavior that matters.
2. Add and Query Cypress data-cy Selectors
Use a quoted attribute value in CSS syntax. Quoting is safest because values can contain hyphens and future edits may introduce characters that need escaping. Cypress retries cy.get() until it finds matching elements or reaches the command timeout.
export function CheckoutButton({ disabled }: { disabled: boolean }) {
return (
<button
type="submit"
data-cy="checkout-submit"
disabled={disabled}
>
Place order
</button>
)
}
cy.get('[data-cy="checkout-submit"]')
.should('be.enabled')
.click()
CSS supports exact, prefix, suffix, and substring attribute matching:
| Selector | Meaning | Recommendation |
|---|---|---|
[data-cy="invoice-row"] |
Exact value | Default choice |
[data-cy^="invoice-"] |
Starts with value | Use for controlled families only |
[data-cy$="-submit"] |
Ends with value | Broad, use sparingly |
[data-cy*="dialog"] |
Contains value | Can hide ambiguous naming |
[data-cy] |
Attribute exists | Useful for audits, not actions |
Prefer exact selectors in tests because they express one contract. Pattern matching can be useful for a deliberate family of elements, but it can unexpectedly include new hooks later. If you need all invoice rows, give every row the exact repeated value invoice-row instead of searching for any value that starts with invoice-.
Do not omit the closing quote or interpolate untrusted content into a selector. When a dynamic identifier is necessary, constrain and escape the value, or scope a repeated hook by controlled visible content.
3. Design a data-cy Naming Convention
Use lowercase kebab case based on feature, component, and purpose. Good values describe why the element matters to a test: checkout-submit, invoice-status, account-menu, and password-error. A reader should understand the target without opening the markup.
Avoid names tied to implementation or appearance:
| Weak name | Problem | Better name |
|---|---|---|
blue-button |
Color can change | checkout-submit |
right-panel |
Layout can move | order-summary |
mui-btn-7 |
Framework detail | profile-save |
third-item |
Order is volatile | cart-row plus scope |
click-here |
No domain meaning | invite-send |
john@example.test |
Personal data in DOM | account-email |
Choose a predictable vocabulary. Action hooks can use feature-verb, such as project-create. Display hooks can use feature-noun, such as project-title. State remains an assertion, not necessarily part of the selector. Prefer data-cy="project-row" data-state="archived" over separate project-row-archived hooks when the same component changes state.
Do not encode a complete DOM path into the value. settings-account-profile-form-save-button is hard to read and preserves incidental hierarchy. Use a stable root profile-form and a scoped child save, or use one globally meaningful profile-save based on your uniqueness policy.
Document five examples and five prohibited patterns in the repository. A short convention used in code review works better than a large catalog nobody follows.
4. data-cy vs Text, Roles, IDs, and CSS Classes
A resilient suite uses locator strategies according to the behavior under test. data-cy is deliberately test-facing. Visible text and accessible roles are user-facing. IDs can be stable but often carry styling, label, URL-fragment, or JavaScript responsibilities. Classes are usually styling contracts.
| Locator | Strength | Main risk |
|---|---|---|
data-cy |
Stable and explicit | Can ignore user semantics if overused |
| Visible text | Validates copy users see | Copy and localization can change |
| Accessible role and name | Aligns with assistive interaction | May require a locator plugin in Cypress tests |
HTML id |
Native and often unique | May be coupled to labels, scripts, or styling |
| CSS class | Already present | Refactors and generated names cause churn |
| DOM position | Quick prototype | Extremely brittle to layout changes |
Ask: If the text changed, should this scenario fail? A test for legal consent wording should locate or assert the text. A checkout-flow test that does not care whether the button says Place order or Complete purchase can use data-cy="checkout-submit" and assert the resulting order.
User-centered locators can expose accessibility defects, but data hooks are useful for canvas controls, composite widget internals, localized interfaces, animated components, and stable feature boundaries. They are complementary, not competing dogmas.
Never select by a test hook and stop at existence. After clicking checkout-submit, assert the confirmation heading, order identifier, URL, network effect, or another business outcome. The UI automation locator strategy guide provides a broader decision framework.
5. Scope Repeated Components and Lists
Repeated rows should normally share the same component hook. Locate the collection, narrow it to the correct business record, then query an action inside that scope. Do not generate a unique hook for every database row unless the stable identifier itself is part of a controlled fixture contract.
<article data-cy="project-card">
<h3 data-cy="project-name">Mobile Checkout</h3>
<button data-cy="project-menu">Actions</button>
</article>
<article data-cy="project-card">
<h3 data-cy="project-name">Billing Portal</h3>
<button data-cy="project-menu">Actions</button>
</article>
cy.contains('[data-cy="project-card"]', 'Billing Portal')
.within(() => {
cy.get('[data-cy="project-menu"]').click()
})
cy.get('[data-cy="project-actions-dialog"]')
.should('be.visible')
.and('contain.text', 'Billing Portal')
cy.contains(selector, text) finds an element matching the selector that contains the requested text. Use exact enough fixture content to avoid one project name being a substring of another. Another pattern captures all cards and filters with jQuery, but query-level scoping is usually easier to diagnose.
Avoid eq(2) or nth-child(3) unless list order is the actual requirement. Inserted fixtures, sorting, and pagination can change positions. If the third result matters, first assert the ordered names and then use position deliberately.
Virtualized lists only render a subset of rows. A correct hook cannot locate an unmounted record. Use the product's search or scrolling workflow, then query the rendered row. Data hooks improve identity, not application state or rendering timing.
6. Build Testable React and Vue Components
Component libraries should forward data-cy to the meaningful DOM node. A custom component can silently drop unknown props when it destructures a limited set, leaving tests to query a hook that exists in source but not in rendered HTML.
type IconButtonProps = {
label: string
onClick: () => void
'data-cy'?: string
}
export function IconButton({
label,
onClick,
'data-cy': dataCy,
}: IconButtonProps) {
return (
<button
type="button"
aria-label={label}
data-cy={dataCy}
onClick={onClick}
>
<span aria-hidden="true">...</span>
</button>
)
}
The hook lands on the actionable button, not a decorative icon or outer layout wrapper. The button also retains its accessible name. A test contract should reinforce good markup, not compensate for a noninteractive div with a click handler.
In Vue, declare or forward the attribute to the root target when attribute inheritance is disabled:
<template>
<button type="button" :data-cy="dataCy" @click="$emit('save')">
Save
</button>
</template>
<script setup lang="ts">
defineProps<{ dataCy: string }>()
defineEmits<{ save: [] }>()
</script>
Give reusable components a hook from their screen context or a semantic default only when that default remains unique in scope. Do not place the same value on both wrapper and control. If both need hooks, define separate purposes such as date-picker and date-picker-input.
7. Create a Typed Custom data-cy Command
A small custom command can standardize exact selector construction and reduce punctuation noise. It should remain transparent and preserve cy.get() options. Cypress does not ship cy.getByCy() automatically, so add it explicitly before using it.
// 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/checkout.cy.ts
cy.getByCy('checkout-submit').should('be.enabled').click()
cy.getByCy('order-confirmation').should('be.visible')
Only interpolate controlled hook names. If values can contain quotes or backslashes, use a robust CSS escaping strategy rather than accepting arbitrary user input. In most teams, hook values are code constants and a constrained naming rule makes the helper safe.
A helper is optional. Direct cy.get('[data-cy="..."]') is universally recognizable and produces standard Command Log output. Avoid building a locator framework with hidden retries, automatic first(), fallback selectors, or silent text matching. Such behavior conceals duplicate contracts and makes failures harder to explain.
If your team already uses data-test or data-testid consistently, do not rename everything solely to copy a convention. Consistency and contract quality matter more than the suffix.
8. Combine Hooks With State and Relationship Assertions
Use data-cy to identify the component, then assert independent DOM or accessibility state. The selector should remain stable while the assertions describe behavior that is expected to change.
cy.get('[data-cy="billing-toggle"]')
.should('have.attr', 'aria-checked', 'false')
.click()
.should('have.attr', 'aria-checked', 'true')
cy.get('[data-cy="billing-fields"]')
.should('be.visible')
.find('[data-cy="billing-country"]')
.select('Canada')
Do not encode state into a selector and then change selectors after every action unless different elements truly exist. A stable billing-toggle hook with aria-checked assertions is clearer than billing-toggle-off becoming billing-toggle-on.
Parent-child relationships can communicate component boundaries:
cy.get('[data-cy="payment-form"]').within(() => {
cy.get('[data-cy="card-number"]').type('4242424242424242')
cy.get('[data-cy="payment-submit"]').click()
})
within() scopes later queries in its callback, but its subject behavior has rules of its own. Do not attempt to change the outer subject by returning a different element from the callback. Use find() for a fluent descendant chain or start a new query after the block.
Shadow DOM components require entering the shadow root with supported Cypress options or .shadow() before querying internal hooks. Prefer a public component behavior when possible, because hooks buried inside a third-party component can couple tests to implementation you do not own.
9. Govern Selector Contracts at Team Scale
Treat hooks as a small public API between product and test code. A pull request that removes data-cy="subscription-cancel" should update the consuming tests or explain why the user workflow no longer exists. Cosmetic changes should preserve the hook. A genuine domain change can rename it with the feature.
Keep a short policy in the repository:
- Use lowercase kebab case.
- Name by business purpose.
- Keep values unique within documented scope.
- Never include secrets, customer data, tokens, or unstable database IDs.
- Prefer user-facing locators when their semantics are the requirement.
- Assert an outcome after each significant action.
- Review hook and test changes together.
Linting can catch accidental dependence on classes and encourage data attributes. The Cypress ESLint ecosystem includes a rule for requiring data selectors, but enable rules according to your locator strategy. A policy that mandates hooks for every target can discourage role, label, and text coverage.
Search the repository before renaming a hook:
rg 'data-cy="subscription-cancel"|getByCy[(]"subscription-cancel"' src cypress
Static search will not find values assembled dynamically, which is another reason to prefer literal exact names. Component tests can verify that important reusable controls forward their hook to the intended DOM node. End-to-end tests should continue proving the user workflow, not merely the presence of attributes.
10. Debug Missing, Duplicate, and Flaky Selectors
A selector failure falls into one of three broad groups. Zero matches means the hook is not rendered in the current scope, the value is wrong, the component dropped the attribute, the page is in another frame or origin, or the expected state has not occurred. Multiple matches mean the contract or scope is ambiguous. Actionability failures mean the element was found but is hidden, covered, moving, disabled, or detached.
Start by inspecting the rendered DOM and Command Log. Confirm the exact value and current URL. Check component prop forwarding, conditional rendering, feature flags, shadow roots, and iframe boundaries. Add a temporary count assertion:
cy.get('[data-cy="checkout-submit"]')
.should('have.length', 1)
.and('be.visible')
.and('be.enabled')
Do not automatically add first() when multiple matches appear. That hides an under-specified target and can click the wrong element. Scope to the active dialog, correct row, or visible form. Do not add force: true until you understand why Cypress considers the target non-actionable. The user may be unable to interact with it for the same reason.
If the element appears after a request, assert an observable state or wait for a registered intercept alias. Avoid fixed sleeps. A stable selector does not fix unpredictable application state. For advanced waiting patterns, read Cypress intercept and wait examples.
11. Review and Refactor Selector Quality
A selector can be stable and still be poor. Review the hook together with the action and assertion. Ask whether the value communicates business purpose, whether its scope is obvious, whether the target is the actual interactive element, and whether the test proves an outcome beyond existence.
Use a focused review checklist:
- Would the name survive a CSS framework, layout, or copy refactor?
- Is the value unique in its documented scope?
- Does a repeated hook represent a reusable component rather than an accidental duplicate?
- Could a role, label, or required text express the behavior more directly?
- Does the value avoid secrets, personal data, random IDs, and environment-specific details?
- Does the test re-query after an action that can replace the node?
- Does the failure point identify the missing contract clearly?
When refactoring, change one workflow at a time. Replace a generated class with a hook, remove obsolete helper fallbacks, and run the spec in both interactive and headless modes. Search for every old consumer before deleting the prior markup. If the test remains flaky, inspect data creation, network state, animation, and test isolation rather than adding more selector syntax.
Measure success through maintenance outcomes, not the number of attributes added. Useful evidence includes fewer locator-only failures, clearer review discussions, and selectors that remain unchanged through cosmetic releases. Do not invent a target percentage for data-cy usage. The right mix depends on how much of the suite deliberately verifies user-facing names and semantics.
Interview Questions and Answers
Q: Why are data-cy selectors more stable than CSS classes?
data-cy is an explicit automation contract, while classes normally describe styling and can change during visual refactoring or build processing. The test hook can remain stable when color, layout, or framework classes change. It still requires naming and scope governance.
Q: Does Cypress provide a built-in getByCy command?
No. Use cy.get('[data-cy="value"]') or define and type a custom command. A helper should preserve Cypress query behavior and avoid hidden fallbacks.
Q: When should you prefer visible text over data-cy?
Use text when the wording is part of the requirement and a change should fail the test. Use data-cy when copy is localized or incidental to the workflow. A hook can locate the component while a separate assertion validates important text.
Q: How do you select one row when data-cy values repeat?
I locate the repeated row component, narrow it using a stable business identifier or controlled visible content, then query the child action within that row. I avoid positional selectors unless order is explicitly under test.
Q: Should data-cy values contain database IDs?
Usually no. IDs may be volatile, expose internal information, and make tests depend on generated data. Prefer a repeated component hook plus a controlled fixture label or domain-stable non-sensitive identifier.
Q: How do you debug a data-cy element that Cypress finds but cannot click?
I inspect visibility, coverage, animation, disabled state, detachment, and current scope. I verify whether the problem reflects a real user interaction issue before considering force. Fixed waits do not resolve the underlying actionability cause.
Q: Do data-cy selectors test accessibility?
No. They identify automation hooks and do not prove that roles, names, labels, focus, or keyboard behavior are correct. Keep semantic assertions and dedicated accessibility testing in the quality strategy.
Common Mistakes
- Using CSS classes, generated IDs, or DOM paths when a stable product-owned hook is available.
- Naming hooks after color, position, framework internals, or temporary implementation details.
- Adding hooks to decorative wrappers instead of the meaningful interactive element.
- Duplicating a value accidentally and silencing ambiguity with
first(). - Embedding email addresses, tokens, customer names, or unpredictable record IDs in values.
- Generating many unique selectors when a repeated component plus meaningful scoping is clearer.
- Selecting by
data-cybut never asserting the user-visible or domain outcome. - Using test hooks everywhere and missing broken roles, labels, and required text.
- Forgetting that custom components may not forward attributes to the DOM.
- Adding hard waits when rendering state, network completion, or actionability is the real issue.
- Renaming hooks during cosmetic refactors and creating unnecessary test churn.
Conclusion
Cypress data-cy selectors work best as a deliberately small automation API. Add exact, semantic hooks to meaningful DOM elements, query them with standard CSS attribute selectors, and scope repeated components through stable business context. Keep names independent of styling, layout, personal data, and volatile IDs.
Then make the test prove behavior. Combine stable identity with visible, accessible, network, or domain assertions. Adopt a naming policy, review hook changes with their consumers, and migrate brittle selectors one workflow at a time.
Interview Questions and Answers
What problem do data-cy selectors solve?
They provide an explicit automation identity that is independent of CSS styling, DOM position, and incidental copy. This reduces selector churn during visual refactoring. They do not replace behavioral or accessibility assertions.
How would you define a selector naming standard?
I use lowercase kebab case based on feature and purpose, such as billing-submit. I define expected uniqueness scope and prohibit visual terms, framework names, secrets, personal data, and volatile IDs. I include examples in repository guidance and code review.
How do you handle repeated data-cy selectors?
I treat the repeated value as a component hook, scope it by a meaningful business identifier, and query the child within that scope. I avoid first(), eq(), and DOM position unless order is the requirement.
When would you not use data-cy?
I prefer visible text when copy is under test and semantic locators when role or label behavior is central. I also avoid adding a hook when a stable existing user-facing locator already expresses the requirement clearly.
What does a custom getByCy command need?
It needs a Cypress.Commands.add implementation, a TypeScript Chainable declaration, and support-file loading through the project's normal setup. It should return cy.get with the standard options and avoid masking duplicate or missing targets.
How would you diagnose a flaky data-cy click?
I separate selector matching from actionability. I inspect rendered DOM, match count, current scope, visibility, overlays, disabled state, animation, and detachment. I wait on observable application state instead of adding a fixed sleep or force click.
Are test hooks an accessibility strategy?
No. A data attribute says nothing about accessible name, role, focus, keyboard behavior, or contrast. I use hooks for stable identity and maintain separate semantic assertions, accessibility automation, and manual evaluation.
Frequently Asked Questions
How do I select data-cy in Cypress?
Use a standard CSS attribute selector, for example cy.get('[data-cy="checkout-submit"]'). Cypress retries the query according to its normal query and timeout behavior.
Is data-cy built into Cypress?
data-cy is a conventional HTML data attribute, not a special Cypress command. Cypress recommends stable data attributes, and you query them through cy.get() unless your project adds a custom helper.
What is a good data-cy naming convention?
Use lowercase kebab case based on business purpose, such as checkout-submit or invoice-status. Avoid color, position, framework names, secrets, personal data, and volatile identifiers.
Can multiple elements have the same data-cy value?
Yes, repeated components may intentionally share a value such as project-card. Scope to the correct card, row, form, or dialog before selecting a child action.
Should I use data-cy or cy.contains?
Use cy.contains when visible wording is part of the expected behavior. Use data-cy when the copy is incidental, localized, or likely to change while the business action remains the same.
Can I create cy.getByCy?
Yes. Add and type a custom command that returns cy.get with the exact data-cy attribute selector and standard options. Keep the helper small and do not add hidden fallback or first-match behavior.
Should data-cy attributes be removed from production?
Not automatically. They are normally harmless if values contain no sensitive information, and keeping tested markup identical to deployed markup has value. Follow an explicit architecture and security policy.