Cypress: This Element Is Being Covered by Another Element

9 min readupdated July 24, 2026

Your test clicks a button that is plainly on the screen, and Cypress stops:

CypressError: cy.click() failed because this element is being covered
by another element:

<div class="cookie-banner">...</div>

Almost everyone's first move is to add { force: true }. It works instantly, which is the problem. Before you do it, spend two minutes here, because the error has already told you the answer.

Cypress checked something on your behalf

Before Cypress acts on an element, it runs a set of actionability checks. It confirms the element is:

  • visible
  • not disabled
  • not animating
  • not covered by another element
  • scrolled into view

If any check fails, Cypress keeps retrying until the timeout, which is four seconds by default. So this error does not mean "the element was covered for a moment". It means the element was still covered after four seconds of retrying. That is a stable condition, not a timing accident, and it changes where you should look.

Why does Cypress bother? Because the alternative is a test that passes while a banner covers your entire checkout form. The most expensive test is one that passes on a broken page, and these checks exist to stop that happening.

Read the element it names

This is the part people scroll past. Cypress does not just say something is covering the element. It prints which element:

<div class="cookie-banner">...</div>

That is your cause, identified. There is more in the command log too: hover over the failed click step and Cypress shows you the page at that exact moment, with the covering element highlighted. You can see the banner sitting on top of your button.

No other tool makes this as easy, so use it before changing any code.

What is usually covering it

A cookie or consent banner. The most common by a wide margin, and it blocks every test in the suite rather than just this one.

A sticky header or footer. These bite when the element is at the very edge of the viewport. Cypress scrolls the element into view, it lands flush against the top, and the sticky header sits right on top of it. The element is technically in view and still unclickable.

A toast or notification from the previous step. You save a contact, a success message slides in over the corner, and your next click lands in that corner. This one is intermittent, so it usually gets blamed on flakiness.

A modal that is still animating out. You closed it, and it is fading over 300ms. Cypress retries for four seconds, so this only bites when the animation is slow or stalls.

An invisible backdrop left behind by a closed dialog. The dialog looks gone, but its full-screen overlay is still in the page. This one is a genuine application bug, and your test just found it. Take it to the developers rather than working around it.

The fixes, in the order to try them

First: remove the blocker in setup, not in the test

If a consent banner blocks everything, do not dismiss it in fifty tests. Stop it appearing:

beforeEach(() => {
  cy.setCookie('consent', 'accepted');
  cy.visit('/');
});

This is faster than clicking it away, and it cannot half-fail. Setting the cookie before cy.visit means the banner never renders at all.

Second: wait for the blocker to go, properly

When something must disappear on its own, assert that it is gone. Cypress retries assertions, so this waits without a fixed delay:

cy.get('.modal-backdrop').should('not.exist');
cy.get('[data-testid="save"]').click();

Use not.exist when the element is removed from the page, and not.be.visible when it stays but is hidden. Picking the wrong one gives a confusing failure, so check which your app does.

For a toast that covers things, waiting for it to clear is honest, and closing it is often better:

cy.get('[data-testid="toast"]').should('not.exist');

Third: deal with sticky headers by scrolling differently

If a fixed header keeps catching your element, change where Cypress scrolls it to:

cy.get('[data-testid="save"]').scrollIntoView({ block: 'center' }).click();

Centring the element keeps it clear of both a sticky header and a sticky footer. You can also set this globally in cypress.config.js with the scrollBehavior option, which is worth doing if your app has a fixed header on every page:

module.exports = defineConfig({
  e2e: {
    scrollBehavior: 'center',
  },
});

One global setting can remove a whole class of failure from your suite.

Fourth: check you matched the element you meant

Responsive layouts often render two copies of a menu, one for mobile and one for desktop, hiding one with CSS. If your selector matched the hidden copy, you get a visibility error rather than a covered one, but the investigation is the same:

cy.get('.menu-link').should('have.length', 1);

If that fails, your selector is too broad. Scope it to the container it lives in instead of working around the ambiguity.

What force: true actually does

cy.get('[data-testid="save"]').click({ force: true });

It is worth knowing precisely, because the documentation phrasing is easy to underestimate. force: true turns off every actionability check. Not just the covered check. Visible, disabled, animating, covered, scrolled into view: all of them, all skipped. Cypress fires the event at the element and moves on.

So a test with force: true will pass when:

  • a banner covers the button completely
  • the button is hidden with display: none
  • the button is disabled and a user could never press it
  • the element has zero width and height

Every one of those is a bug your test was built to catch. You have not fixed the failure. You have asked Cypress to stop mentioning it.

There is a subtler cost too. force: true skips the events a real click produces first, so mousedown, mouseup and hover behaviour never fire. Components that depend on those, which includes many dropdowns and drag interactions, behave differently under a forced click than under a user.

When it is genuinely fine: an element you truly cannot control is permanently in the way, such as a third-party widget you cannot change, and you have confirmed a real user is unaffected. Then use it, and leave a comment saying why:

// The chat widget overlaps this button and we cannot configure its position.
// Verified manually that real users can click it on all supported viewports.
cy.get('[data-testid="save"]').click({ force: true });

That comment is the difference between a considered decision and a silenced test. A reviewer can check your reasoning. A bare force: true tells them nothing.

The related errors

Same family, same thinking:

  • "element is not visible because it has CSS property: display: none". You matched a hidden copy, or the element has not rendered yet.
  • "element has an effective width and height of 0 x 0 pixels". Usually a wrapper element rather than the control itself. Select the inner button.
  • "element is disabled". Wait for whatever enables it, normally a valid form, instead of forcing the click.
  • "element is being covered by another element" when nothing looks on top. Check for an invisible overlay left behind by a closed dialog.

Remember this

Cypress retries for four seconds before this error, so a covered element is a stable condition rather than bad luck. Read the element it names, because that is your cause. Remove blockers in setup, assert the overlay is gone, and centre the scroll when a sticky header is the problem. Keep force: true for cases you can justify in a comment, because it switches off every check, not only the one that failed.

The Cypress track goes deeper on the retrying that sits underneath all of this, including why it makes some assertions safe and others useless: how retrying really works.

Related: Selenium element not interactable and how to fix flaky tests.

Reference: Cypress actionability.

now prove it

Prove a contact was actually saved

Reading is the easy part. Write the test yourself, then we break the app on purpose to check it would really catch the bug.

Solve this problem →

Keep reading