Cypress: Element Is Detached from the DOM — How to Fix It
CypressError: cy.click() failed because the element has been detached from the DOM
What causes this
Cypress found the element, but by the time it tried to interact with it, the element was removed from the DOM and re-created. This is extremely common with React, Vue, and other frameworks that re-render components — the old DOM node gets replaced with a new one, but Cypress is still holding a reference to the old one.
Typical scenarios:
- A state update causes a re-render between
cy.get()and.click() - An API response arrives and replaces the content
- A loading spinner disappears and the real content mounts
- A parent component re-renders, destroying and recreating child elements
Fix 1: Don’t store element references
The most common mistake — storing an element and using it later:
// ❌ Element might be stale by the time you click
cy.get('.button').then($btn => {
cy.get('.input').type('hello'); // This might cause a re-render
cy.wrap($btn).click(); // Detached!
});
// ✅ Re-query the element — Cypress auto-retries
cy.get('.input').type('hello');
cy.get('.button').click();
Cypress commands that re-query (like cy.get()) automatically wait for the element to exist. Wrapped references don’t.
Fix 2: Use assertions to wait for stability
Tell Cypress to wait until the element is in a stable state before interacting:
// ✅ Wait for the element to be visible and stable
cy.get('[data-testid="submit-btn"]')
.should('be.visible')
.and('not.be.disabled')
.click();
The .should() assertion retries until it passes, which means Cypress will wait for re-renders to finish.
Fix 3: Use data-testid attributes
cy.get('.button') might match different elements as the DOM changes. Use stable selectors:
// ❌ Class-based selectors are fragile
cy.get('.btn-primary').click();
// ✅ data-testid is stable across re-renders
cy.get('[data-testid="submit-btn"]').click();
Fix 4: Wait for the re-render trigger to complete
If you know what causes the re-render, wait for it:
// Wait for the API call to finish before clicking
cy.intercept('GET', '/api/data').as('getData');
cy.visit('/page');
cy.wait('@getData');
cy.get('[data-testid="action-btn"]').click();
Fix 5: Use cy.contains() for text-based elements
cy.contains() re-queries the DOM on each retry:
// ✅ Cypress will find the element even after re-renders
cy.contains('button', 'Submit').click();
How to prevent it
- Never store DOM references with
.then()and use them later — always re-query - Use
data-testidattributes for stable element selection - Add
.should('be.visible')before interactions to wait for re-renders - Use
cy.intercept()andcy.wait()to synchronize with API calls - If a specific component causes frequent detachment issues, consider adding a loading state that Cypress can wait for