I'm trying to use Cypress input typing with an <ion-input> element in my app, but I'm running into an error

Here’s the markup:

<ion-input data-cy="email" type="email" class="border" placeholder="EMAIL"></ion-input>

And the test code:

const typedText = 'test@email.com';

cy.get('[data-cy=email]')
  .type(typedText, { force: true })
  .should('have.value', typedText);

But Cypress throws this error:

CypressError: cy.type() failed because it requires a valid typeable element.

How can I properly type text into an <ion-input> using Cypress input commands?

Do I need to target a shadow DOM or the internal native input somehow?

Yeah, I ran into this exact problem when working with Ionic apps. The issue is that <ion-input> is a custom web component — it wraps a native <input> inside its Shadow DOM. So, you can’t type directly into the <ion-input> tag itself.

What worked for me was querying the native input like this:

cy.get('ion-input[data-cy="email"]')
  .shadow()
  .find('input')
  .type('test@email.com')
  .should('have.value', 'test@email.com');

This tells Cypress to pierce into the shadow DOM and access the actual input field. Once I started doing this, all my Cypress input tests worked like a charm.

So, in one of my previous projects, I found typing didn’t always trigger the necessary events inside <ion-input>.

Instead of using .type(), I ended up manually setting the value and firing the ionChange event.

Something like:

cy.get('ion-input[data-cy="email"]')
  .then($el => {
    const input = $el[0].shadowRoot.querySelector('input');
    input.value = 'test@email.com';
    input.dispatchEvent(new Event('input', { bubbles: true }));
    input.dispatchEvent(new Event('ionChange', { bubbles: true }));
  });

It feels a bit hacky, but it worked consistently and didn’t rely on Cypress input mechanics not recognizing Shadow DOM.

I faced this when testing an Ionic React project.

The solution that worked for me was turning on Cypress’s experimental shadow DOM support. Just update your cypress.config.js like this:

export default defineConfig({
  e2e: {
    experimentalModifyObstructiveThirdPartyCode: true,
    includeShadowDom: true
  }
});

Then your test can use .shadow() as mentioned earlier.

From there, Cypress input interactions start working properly since it understands how to deal with components like <ion-input>. It made a noticeable difference once I enabled those flags.