Cypress Selectors and Querying Elements
Querying elements is the foundation of every Cypress test. Whether you're finding a button to click, a form field to fill, or a message to verify, you'll use Cypress's query API—cy.get(), cy.contains(), cy.find()—dozens of times per test. Choosing the right selector strategy makes tests fast to write, maintainable over time, and resilient to design changes.
In my experience testing React applications with hundreds of components, I've found that the most maintainable tests use semantic selectors (data-testid attributes) over fragile CSS classes or deeply nested DOM structures. This article covers the selector strategies that scale.
The Three Main Query Commands
cy.get(selector) — Find one or more elements by CSS selector. This is the most common query:
// By CSS class
cy.get('.button-primary').click()
// By HTML attribute
cy.get('[data-testid="submit-button"]').click()
// By tag name
cy.get('button').click()
// By ID
cy.get('#main-heading').should('contain', 'Welcome')
cy.contains(text) — Find an element by its visible text content. Use this for user-centric tests:
// Find any element with this exact text
cy.contains('Submit').click()
// Find a specific element type with this text
cy.contains('button', 'Submit').click()
// Find by partial text (regex)
cy.contains(/Submit|Send/).click()
cy.find(selector) — Find elements within a previously queried element (chain after cy.get()). Use this to narrow scope:
// Get the form, then find the submit button within it
cy.get('form').find('[data-testid="submit-button"]').click()
// Or chain multiple finds
cy.get('.modal').find('button').contains('Close').click()
Selector Strategy Hierarchy
Here's the order of preference for reliable selectors:
1. data-testid Attributes (Most Reliable)
Add data-testid to components for testing. These attributes persist even when CSS classes change:
// Component
<button data-testid="login-submit">Sign In</button>
// Test
cy.get('[data-testid="login-submit"]').click()
Advantages: Won't break if styling changes, explicit and readable, semantically separates test concerns from styling.
2. Semantic HTML Attributes (Role, ID, Name)
Use HTML attributes that describe purpose:
// Component
<button id="main-nav-toggle" aria-label="Toggle navigation">Menu</button>
// Test — by ID (stable)
cy.get('#main-nav-toggle').click()
// Or by role (accessible and user-centric)
cy.get('[role="button"]').contains('Menu').click()
3. User-Centric Text Queries (cy.contains())
Query by visible text that users see:
// Component
<button>Submit Form</button>
// Test — find by text
cy.contains('button', 'Submit Form').click()
Advantages: Mirrors how users interact (they see text, not selectors). Disadvantages: breaks if copy changes, ambiguous if multiple elements have the same text.
4. CSS Classes (Avoid if Possible)
CSS classes change when styling is refactored. Only use if data-testid isn't available:
cy.get('.btn.btn--primary').click() // Fragile
5. Deeply Nested Selectors (Avoid)
Never rely on the full DOM hierarchy; it's brittle:
// Bad: breaks if DOM structure changes
cy.get('div > div > form > fieldset > div > input')
// Good: target the input directly with data-testid
cy.get('[data-testid="email-input"]')
Practical Selector Examples
Here's a realistic form component and how to query it:
// Component
export function LoginForm() {
const [email, setEmail] = useState('')
return (
<form data-testid="login-form">
<label htmlFor="email-input">Email</label>
<input
id="email-input"
data-testid="email-input"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<button data-testid="submit-button" type="submit">
Sign In
</button>
</form>
)
}
// Test
it('fills the email form and submits', () => {
mount(<LoginForm />)
// Query the input by data-testid
cy.get('[data-testid="email-input"]').type('[email protected]')
// Query the button by data-testid
cy.get('[data-testid="submit-button"]').click()
})
Notice that we use data-testid for precise, stable element selection. The test doesn't depend on CSS class names or the form's internal structure.
Chaining Queries with find()
When you need to narrow the scope, chain queries:
// Component
<div data-testid="user-profile">
<h1>Profile</h1>
<form data-testid="profile-form">
<input data-testid="name-input" type="text" />
<button data-testid="save-button">Save</button>
</form>
</div>
// Test — query the form, then find the button within it
cy.get('[data-testid="profile-form"]')
.find('[data-testid="save-button"]')
.click()
// Or more concisely:
cy.get('[data-testid="save-button"]').click()
Both approaches work, but if you have multiple save buttons on the page, scoping with find() ensures you're clicking the right one.
Working with Lists and Iteration
When querying multiple elements, use iteration to verify each one:
// Component
<ul data-testid="todo-list">
<li>Learn Cypress</li>
<li>Write tests</li>
<li>Deploy app</li>
</ul>
// Test — verify all items
cy.get('[data-testid="todo-list"] li').should('have.length', 3)
// Test — verify specific item
cy.get('[data-testid="todo-list"] li').eq(0).should('contain', 'Learn Cypress')
// Test — iterate over all items
cy.get('[data-testid="todo-list"] li').each(($item) => {
cy.wrap($item).should('contain.text', /Learn|Write|Deploy/)
})
Use .eq(index) for a specific item, or .each() to iterate over all matches.
Handling Dynamic Content and Waiting
Cypress automatically waits (up to 4 seconds by default) for elements to appear:
// Component loads async data
<div>
{isLoading ? <p>Loading...</p> : <p>{message}</p>}
</div>
// Test — Cypress waits for the message to appear
cy.get('[data-testid="message"]').should('contain', 'Welcome')
If a query takes longer than the timeout, use .should() with a custom timeout:
cy.get('[data-testid="slow-element"]', { timeout: 10000 }).should('exist')
Advanced Selector Techniques
Pseudo-selectors:
// Get all buttons except the first
cy.get('button:not(:first)')
// Get the last button
cy.get('button:last')
// Get buttons with specific attribute value
cy.get('button[type="submit"]')
Attribute selectors:
// Element with attribute containing substring
cy.get('[data-testid*="button"]') // Matches "button-submit", "button-cancel"
// Attribute starts with value
cy.get('[data-testid^="btn"]')
// Attribute ends with value
cy.get('[data-testid$="button"]')
XPath (discouraged):
Cypress supports XPath, but it's slower and less readable. Avoid it:
// Don't do this
cy.xpath('//button[contains(text(), "Submit")]')
// Do this instead
cy.contains('button', 'Submit')
Debugging Selector Issues
When a selector doesn't match, Cypress shows a detailed error with screenshots and suggestions. Use the Cypress DevTools Inspector to debug:
// Open the Cypress Inspector to see element matching
cy.get('[data-testid="my-button"]').debug()
// Or use pause to stop and inspect
cy.get('[data-testid="my-button"]').pause()
In the interactive Cypress UI, hover over commands in the command log to see DOM snapshots and highlighted elements.
Key Takeaways
- Use
data-testidattributes for reliable, maintainable selectors that won't break when styling changes. - Prefer
cy.contains()for user-centric queries and semantic HTML attributes (ID, role) for accessibility-aligned tests. - Avoid deeply nested selectors and CSS class-based queries; they break when DOM structure or styling changes.
- Chain queries with
.find()to narrow scope when targeting specific elements within containers. - Cypress automatically waits for elements; use custom timeouts only for slower operations.
- Use the Cypress Inspector and debug commands to troubleshoot selector issues.
Frequently Asked Questions
What's the best selector strategy for a large, complex application?
Use data-testid consistently across all components. Establish a naming convention (e.g., [role]-[component]-[element] like form-login-submit-button) and document it in your testing guide.
Can I use CSS Grid or Flexbox layout selectors?
CSS layout selectors are unreliable for testing (they're not semantic). Use data-testid or semantic HTML instead. Flexbox/Grid affect DOM appearance, not DOM structure.
Should I use CSS-in-JS library class names for selectors?
No. CSS-in-JS libraries generate unpredictable class names. Use data-testid instead, which is independent of styling.
What if a component is generated by a third-party library and doesn't have data-testid?
Query by role or accessible text:
cy.get('button[aria-label="Close"]').click()
// Or by role if multiple buttons exist
cy.get('[role="dialog"] button[aria-label="Close"]').click()
How do I select an input field by label text?
Use the label's for attribute or query the label then find the input:
// Component
<label htmlFor="password">Password</label>
<input id="password" type="password" />
// Test
cy.get('#password').type('secret')
// Or find by label
cy.get('label').contains('Password').parent().find('input').type('secret')