---
title: "Find elements the way your users do with cy.getByRole()"
url: "https://www.cypress.io/blog/find-elements-the-way-your-users-do-with-cy-getbyrole"
description: "Cypress 16.2.0 adds cy.getByRole() to find elements by role and accessible name, catch inaccessible custom buttons, and move off Testing Library."
---

# Find elements the way your users do with cy.getByRole()

October 14, 2026

By Jennifer Shehane

![Cypress new in 16.2.0, cy.getByRole(), Find elements the way your users do - Query by role and accessible name. Native elements by default. Code example of cy.getByRole with a graphic of a na](https://storage.ghost.io/c/12/89/12899ca1-4786-4eea-b7e1-9c5268baa48a/content/images/2026/10/Cypress---getByRole---Blog-hero-1200--630@2x.png)

Starting in Cypress 16.2.0, `cy.getByRole()` is a built-in query that finds elements by their role and accessible name. The role is what a screen reader announces an element as, such as a button or a link, and the accessible name is the text it announces for it.

This offers a built-in alternative to the `.findByRole()` selector from the popular [Cypress Testing Library](https://github.com/testing-library/cypress-testing-library?ref=cypress-io.ghost.io) plugin. That package no longer has an active maintainer and the previous maintainer suggested Cypress build these queries into Cypress directly.

We agree with that direction. Role-based queries are a core way to write resilient, accessible tests, and they belong in Cypress itself. `cy.getByRole()` is the first of these built-in queries. We plan to bring more of the Testing Library queries teams depend on into Cypress.

## What `cy.getByRole()` does

`cy.getByRole()` finds every element with a given role, such as `button`, `link`, `heading`, or `tab`, and can narrow the results to elements whose accessible name matches. Because those are what assistive technology announces, a test written this way finds elements the way assistive technology does. If a control’s role or label changes, the test fails.

```javascript
describe('account settings', () => {
  it('saves profile changes', () => {
    cy.visit('/settings')
    cy.getByRole('textbox', { name: 'Display name' }).type('Jane Doe')
    cy.getByRole('button', { name: 'Save changes' }).click()
    cy.getByRole('alert').should('have.text', 'Changes saved')
  })
})
```

A few behaviors on how `cy.getByRole()` works:

-   **It yields every match.** Like [`cy.get()`](https://docs.cypress.io/api/commands/get?utm_source=blog&utm_medium=inline-link&utm_content=cy.get%28%29&utm_campaign=getbyrole-launch-oct-2026), `cy.getByRole()` yields all matching elements in document order. Narrow the `name`, or chain `.first()` or `.eq()`, to act on one.
-   **A string name matches the whole name, case-sensitively.** Pass a regular expression to match part of a name or ignore case, or a function for custom logic.
-   **It skips elements hidden from the accessibility tree** by default, such as elements with `aria-hidden="true"` or `display: none`. Pass `hidden: true` to include them.
-   **It retries** like any Cypress query, so it waits for an element to appear and for chained assertions to pass.
-   **It prefers built-in HTML elements.** If a native HTML element exists for a given role, `cy.getByRole()` matches that element only, unless `native: false` is passed.

To narrow by state, such as checked, selected, expanded, or heading level, chain `.filter()`:

```javascript
// The tab that is currently selected
cy.getByRole('tab').filter('[aria-selected="true"]')

// Level 2 headings
cy.getByRole('heading').filter('[aria-level="2"], h2:not([aria-level])')

// A checked native checkbox
cy.getByRole('checkbox').filter(':checked')

// A checked checkbox that gets its role from a role attribute
cy.getByRole('checkbox', { native: false }).filter('[aria-checked="true"]')

// The menu button whose menu is open
cy.getByRole('button').filter('[aria-expanded="true"]')

// A toggle button that is switched on
cy.getByRole('button').filter('[aria-pressed="true"]')

// The link to the page the reader is on
cy.getByRole('link').filter('[aria-current="page"]')

// Every button except disabled ones
cy.getByRole('button').not(':disabled, [aria-disabled="true"]')
```

The [`cy.getByRole()` API reference](https://docs.cypress.io/api/commands/getbyrole?utm_source=blog&utm_medium=inline-link&utm_content=cy.getByRole%28%29%20API%20reference&utm_campaign=getbyrole-launch-oct-2026) covers every option, including shadow DOM, TypeScript, and scoping with `.within()`.

## Why native elements are the default in `cy.getByRole()`

For any role that HTML has a native element for, such as `button`, `link`, `heading`, `dialog`, or `checkbox`, `cy.getByRole()` matches only elements whose tag gives them that role. A `<div role="button">` does not match `cy.getByRole('button')` unless you pass `native: false`.

```html
<button>Save</button>
<div role="button" tabindex="0">Cancel</div>
```

```javascript
// Yields and clicks the native <button>
cy.getByRole('button', { name: 'Save' }).click()

// The <div> has the role only through its role attribute, so it's skipped
cy.getByRole('button', { name: 'Cancel' }).should('not.exist')
```

Why make more strict behavior the default? Because a role-based locator that accepts both elements can cover up accessibility issues that could be easily discovered by your tests. Native controls are the most likely to work well for keyboard-only users, including many people with motor disabilities, or for screen reader users, because the browser handles the behavior automatically and consistently.

On the other hand, custom controls may implement a `role` attribute in order to satisfy a byRole selector, and often skip the rest of the required behavior. This creates a false impression that something is accessible.

### What a `role` attribute does not support natively

Adding `role="button"` to a `<div>` changes one thing: assistive technology announces the element as a button. As [MDN’s documentation of the button role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/button_role?ref=cypress-io.ghost.io) explains, the role does not add keyboard handling or other button behavior. The application code has to account for all of it.

A native `<button>` gives you these behaviors built in:

-   Automatically joins the keyboard tab order.
-   Activates with both the Enter and Space keys.
-   Fires a `click` event for keyboard activation, not only for mouse clicks.
-   Supports the `disabled` attribute, which removes the button from the tab order and blocks activation.
-   Submits the surrounding form.

A `<div role="button">` gets none of these. To match a native button, a developer has to add `tabindex="0"`, a `keydown` handler for both keys, logic to stop Space from scrolling the page, and their own disabled state handling. Each of those is a place for a bug to hide, and a mouse-driven test won’t notice any of them, so if the accessibility-friendly byRole locator is passing, it can be easy to miss the risk.

### What people experience when the native element doesn’t match the role

Here’s what that gap looks like for real users of a partially-implemented button, for example a `<div role="button">` that only has a click handler:

-   **Keyboard-only users** press Tab and skip right past the control. Without `tabindex`, the browser never puts it in the tab order, so there’s no way to reach it. People who can’t use a mouse, including many people with motor disabilities, are stuck.
-   **Screen reader users** may reach the content and hear “Save, button” and do what anyone would: press Enter or Space. Depending on their screen reader and how they’re navigating, nothing may happen, because the page only listens for mouse clicks. They will not know if they successfully operated the button.
-   **Anyone using Space to activate it** may watch the page scroll down instead, because Space scrolls the page unless the developer prevents it.
-   **Users who encounter a “disabled” state** may still be able to activate it. Setting `aria-disabled="true"` describes a state to screen readers, but doesn’t block clicks or key presses the way the native `disabled` attribute does, allowing for unintentional activation.

None of this shows up in a test that clicks the element with a simulated mouse event. That’s exactly why we made native matching the default. If someone replaces a `<button>` with a `<div role="button">`, your existing `cy.getByRole('button', ...)` test fails and points you at the change, and you can decide whether it was intentional and if more tests are needed.

The broader principle comes from web accessibility: The [first rule of ARIA](https://www.w3.org/TR/using-aria/?ref=cypress-io.ghost.io#rule1) advises using a native HTML element whenever one already has the semantics and behavior you need, instead of rebuilding it with an ARIA `role` attribute. The [W3C ARIA Authoring Practices Guide button pattern](https://www.w3.org/WAI/ARIA/apg/patterns/button/?ref=cypress-io.ghost.io) shows how much work a custom button takes to get right.

### When to pass `native: false` to `cy.getByRole()`

In some cases a `role` attribute is the right call, or at least not yours to change. Pass `native: false` to also match elements that get their role from a `role` attribute:

```javascript
// A dialog rendered by a component library you don't control
cy.getByRole('dialog', { name: 'Delete project', native: false }).should('be.visible')
```

A few common patterns need `native: false` even when they follow the ARIA authoring practices, because HTML has a different native element for the role:

-   An autocomplete `<input role="combobox">`. The native combobox is a `<select>` or an `<input>` with a `list` attribute.
-   An icon `<svg role="img">`. The native image is an `<img>`.

For widget roles that HTML has no element for, such as `tab`, `menuitem`, or `switch`, a `role` attribute is the only option, so `cy.getByRole()` always matches it. You don’t need `native: false` for those.

If your app is built on a component library that re-implements native elements with `role` attributes, you don’t have to pass `native: false` on every call. You can [overwrite the query](https://docs.cypress.io/api/commands/getbyrole?utm_source=blog&utm_medium=inline-link&utm_content=overwrite%20the%20query&utm_campaign=getbyrole-launch-oct-2026#Change-the-native-default-for-your-project) in your support file to make `native: false` your project’s default. A test can still pass `native: true` to require a native element.

```javascript
// cypress/support/commands.js
Cypress.Commands.overwriteQuery(
  'getByRole',
  function (originalFn, role, options = {}) {
    return originalFn.call(this, role, { native: false, ...options })
  }
)
```

## Moving off of Cypress Testing Library

### Do you need to migrate `cy.findByRole()` to `cy.getByRole()` right now?

No. Cypress Testing Library still works with Cypress 16.2.0. It registers only `findBy*` queries, so it doesn’t conflict with `cy.getByRole()`, and you can migrate one spec at a time.

Most calls translate by renaming the query:

```javascript
// Before: Cypress Testing Library
cy.findByRole('button', { name: 'Place order' }).click()
cy.findAllByRole('listitem').should('have.length', 3)
cy.findByRole('tab', { selected: true }).should('have.text', 'Shipping')

// After: cy.getByRole()
cy.getByRole('button', { name: 'Place order' }).click()
cy.getByRole('listitem').should('have.length', 3)
cy.getByRole('tab').filter('[aria-selected="true"]').should('have.text', 'Shipping')
```

There are a few differences that could cause a one-to-one replacement to fail:

1.  **Native elements by default.** A test that found a `<div role="button">` with `findByRole()` fails after migrating. Decide for each one whether the app should use the native element, or the test should pass `native: false`. I’d encourage you to treat this list as an accessibility review, not a chore. To keep your current behavior while you migrate, overwrite the query to default to `native: false`, as shown earlier.
2.  **Every match, not one.** `cy.getByRole()` yields every match instead of failing when more than one element matches. Narrow the `name` or chain `.first()` to act on one.
3.  **State options become `.filter()`.** The `checked`, `selected`, `pressed`, `expanded`, `current`, `busy`, `level`, and `value` options move to a chained `.filter()`. If you pass one of these options, the error tells you which filter to use.

The [migration guide in the `cy.getByRole()` docs](https://docs.cypress.io/api/commands/getbyrole?utm_source=blog&utm_medium=inline-link&utm_content=migration%20guide&utm_campaign=getbyrole-launch-oct-2026#How-to-migrate-from-Testing-Library-findByRole-to-cygetByRole) has before-and-after examples and a copyable prompt you can give an AI agent to do the migration.

### Custom command failing with `cy.getByRole()`

If your project already defines its own `getByRole` with `Cypress.Commands.add()` or `Cypress.Commands.addQuery()`, that call throws an error in Cypress 16.2.0, because `getByRole` is now a built-in query. Rename your command, or remove it and use the built-in one.

## How `cy.getByRole()` and Cypress Accessibility fit together

`cy.getByRole()` only checks the elements you locate in a test. [Cypress Accessibility](https://www.cypress.io/accessibility?utm_source=blog&utm_medium=inline-link&utm_content=Cypress%20Accessibility&utm_campaign=getbyrole-launch-oct-2026) widens the view. It runs axe-core checks against every unique page state your recorded tests reach, with no changes to your test code, and reports the results in Cypress Cloud. That catches problems like missing labels, low color contrast, and invalid ARIA on parts of the page no test explicitly asserts.

Put together, it looks like this:

-   **`cy.getByRole()`** helps ensure the elements your tests use or assert about have a clear accessibility purpose and name.
-   **Cypress Accessibility** audits every state your tests reach, including everything they don’t interact with or make assertions about.

`cy.getByRole()` is built into the Cypress app and works without Cypress Cloud. Cypress Accessibility runs with Cypress Cloud. You can adopt either one on its own, and each makes the other more useful, because you will get a more detailed axe-core report if your elements have recognizable roles with associated accessibility expectations, and don’t appear to be generic content.

## What to do next

If you’re starting a new test, reach for `cy.getByRole()` first. If you already use Cypress Testing Library, there’s no rush, but migrating gives you a supported query and an accessibility check on every control your tests touch. And if a migrated test fails because of the native default, take that seriously. It may be the first time your test suite noticed something a keyboard user has been running into all along.

Upgrade to Cypress 16.2.0, then read the [`cy.getByRole()` documentation](https://docs.cypress.io/api/commands/getbyrole?utm_source=blog&utm_medium=inline-link&utm_content=cy.getByRole%28%29%20documentation&utm_campaign=getbyrole-launch-oct-2026) to get started. We’d love to hear how it goes, and which Testing Library queries you’d like to see built in next.

## Frequently asked questions

### What is `cy.getByRole()` in Cypress?

`cy.getByRole()` is a built-in Cypress query that finds DOM elements by their role and, optionally, their accessible name. The role is what a screen reader announces an element as, such as a button or a link. It yields every matching element and retries until the elements exist and chained assertions pass.

### Which version of Cypress includes `cy.getByRole()`?

Cypress 16.2.0 and later.

### What about test IDs?

Test IDs like `data-cy` and `data-test` specifically avoid depending on anything about the element, which means they have no opinion about accessibility, HTML, or content. They are useful for maximum stability where those things don’t matter, or for separating different instances of otherwise-similar elements.

When working with `cy.getByRole()`, you may want to chain `.filter('[data-cy="your-test-id"]')` to ensure a specific element is matched if there is a chance that more than one might be on the same page.

### Do I still need `@testing-library/cypress` to find elements by role?

No. `cy.getByRole()` replaces `findByRole()` and `findAllByRole()` without a plugin. If you use other Testing Library queries, such as `findByLabelText()`, keep the package for now. The two can run side by side.

### Why doesn’t `cy.getByRole()` find my `<div role="button">`?

For roles that have a native HTML element, `cy.getByRole()` matches only the native element by default. Pass `{ native: false }` to also match elements that get their role from a `role` attribute, or switch the element to a native `<button>`, which means keyboard and screen reader users get built-in button behavior and you will not need to pass an option. To make `native: false` your project’s default, overwrite the query with `Cypress.Commands.overwriteQuery()`.

### Why is a native `<button>` more accessible than `role="button"`?

`role="button"` only changes how assistive technology announces the element. A native `<button>` is also in the keyboard tab order, activates with Enter and Space, supports the `disabled` attribute, and submits forms. A custom button has to rebuild each of these by hand, and missing any of them can block keyboard and screen reader users.

### How is `cy.getByRole()` different from Testing Library’s `findByRole()`?

`cy.getByRole()` matches native elements by default, yields every match instead of failing on multiple matches, and uses a chained `.filter()` for states such as `checked` and `selected` instead of options.

### How do I find a checked checkbox or a level 2 heading with `cy.getByRole()`?

Chain `.filter()`. For example, `cy.getByRole('checkbox').filter(':checked')` or `cy.getByRole('heading').filter('[aria-level="2"], h2:not([aria-level])')`.

### Does `cy.getByRole()` work inside shadow DOM?

Yes. Pass `includeShadowDom: true`, set the `includeShadowDom` configuration option, or chain `cy.getByRole()` off `.shadow()`.

### Should I use `cy.getByRole()` or `cy.prompt` self-healing to keep tests stable?

They work well together. `cy.getByRole()` ties a test to roles and accessible names, which rarely change by accident, and fails when a control loses native behavior. `cy.prompt` heals steps when an element changes and marks each heal so you can review it. Preferring `role` and `aria-label` in your `ElementSelector` priority helps `cy.prompt` heal less often.

### Does Cypress Accessibility replace `cy.getByRole()`?

No. Cypress Accessibility audits every page state your recorded tests reach, including elements no test touches. `cy.getByRole()` is an explicit technique to conveniently locate elements by their accessible role and get a built-in preference for native browser implementations.

### Will Cypress add other Testing Library queries?

Yes, that’s our plan. `cy.getByRole()` is the first, and we’ll announce others as they’re ready.