Skip to main content
Cypress App

getByRole

Get every DOM element with the given role, optionally narrowed to the elements whose accessible name matches. 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. A test written with cy.getByRole() finds elements the way assistive technology does, and fails if the role or text is changed.

Browsers expose accessible roles for many elements by default, such as a <button> or a <nav>. A role attribute can also set one, which is the only way to give an element a widget role such as tab. For the full list, see the implicit roles of HTML elements and the roles that a role attribute can set.

By default, cy.getByRole() skips two kinds of elements:

  1. Elements hidden from the accessibility tree
  2. Elements that duplicate a native browser role with an ARIA role attribute, such as a <div role="button">

The command has options to find both, but the defaults favor the elements most likely to be accessible to users. This follows the first rule of ARIA, which advises using native browser elements over a custom re-implementation. To change the defaults for your project, overwrite the query or add a custom query of your own.

Syntax​

.getByRole(role)
.getByRole(role, options)

// ---or---

cy.getByRole(role)
cy.getByRole(role, options)

Usage​

Correct Usage

cy.getByRole('button', { name: 'Save' }) // Yield the button named "Save"
cy.get('nav').getByRole('link') // Yield every link inside <nav>

Incorrect Usage

cy.getByRole(/button/) // Errors, the role must be a string
cy.title().getByRole('heading') // Errors, 'title' does not yield DOM element
cy.getByRole('switch checkbox') // Errors, query one role at a time
cy.getByRole('tab', { native: true }) // Errors, HTML has no native tab element
cy.getByRole('heading', { level: 2 }) // Errors, narrow by level with .filter()

Arguments​

role (String)

The role to find, such as 'button', 'heading', 'navigation', or 'tab'. An element has a role when:

  • Its tag implies one, such as <button> for button, <nav> for navigation, or <input type="checkbox"> for checkbox.
  • Its role attribute sets one, such as <div role="tab">. The attribute replaces the implied role. When it lists several roles as fallbacks, the first valid one counts, as in browsers: role="switch checkbox" is a switch, and role="foo button" is a button. With no valid role in the attribute, the element keeps its implied role.

options (Object)

Pass in an options object to change the default behavior of cy.getByRole().

OptionDefaultDescription
nameundefinedMatch only elements whose accessible name matches. Accepts a string, number, regular expression, or function.
hiddenfalseInclude elements hidden from the accessibility tree, such as those with aria-hidden="true".
nativetrue for roles that HTML has an element for, otherwise falseMatch only elements whose HTML tag gives them the role. Pass false to also match elements that have the role only through a role attribute.
logtrueDisplays the command in the Command log
timeoutdefaultCommandTimeoutTime, in milliseconds, to wait for cy.getByRole() to resolve before timing out. Must be a number.
includeShadowDomincludeShadowDom config option valueWhether to traverse shadow DOM boundaries and include elements within the shadow DOM in the yielded results.

cy.getByRole() throws an error for any other option. To narrow the results by an ARIA state such as checked, expanded, or level, chain .filter().

Yields ​

  • cy.getByRole() yields every matching element, in document order.
  • cy.getByRole() is a query, and it is safe to chain further commands.

Examples​

Role​

Find an element by the role the browser gives it​

index.html
<main>
<h1>Account settings</h1>
<button>Save changes</button>
</main>
spec.cy.js
cy.getByRole('main').should('match', 'main')
cy.getByRole('heading').should('have.text', 'Account settings')
cy.getByRole('button').click()

Find a widget by its ARIA role attribute​

HTML has no element for widget roles such as tab, menuitem, or switch, so a page gives an element one with a role attribute. Roles applied this way follow the Accessible Rich Internet Applications (ARIA) spec, so assistive technology can represent custom UI that follows a standard pattern but isn't part of the HTML spec.

Native matching doesn't apply to these roles, so cy.getByRole() finds the element by its role attribute.

index.html
<div role="tablist">
<button role="tab" aria-selected="true">Profile</button>
<button role="tab" aria-selected="false">Billing</button>
</div>
spec.cy.js
cy.getByRole('tab', { name: 'Billing' }).click()

Get every element with a role​

cy.getByRole() yields every match, like cy.get(), so assert on the length or pick one with .first() or .eq().

index.html
<ul>
<li><a href="/profile">Profile</a></li>
<li><a href="/billing">Billing</a></li>
<li><a>Coming soon</a></li>
</ul>
spec.cy.js
// An <a> without an href has no link role, so only two match
cy.getByRole('link').should('have.length', 2)
cy.getByRole('link').eq(1).should('have.attr', 'href', '/billing')

Check every element with a role​

Chain .each() to run the same check against every match, or pass a callback to .should() to assert on the whole set at once. A .should() callback retries until it passes, so it also waits for a list that is still loading.

index.html
<nav aria-label="Account">
<a href="/profile">Profile</a>
<a href="/billing">Billing</a>
<a href="/security">Security</a>
</nav>
spec.cy.js
// Runs once for each of the three links
cy.getByRole('link').each(($link) => {
cy.wrap($link).should('have.attr', 'href').and('match', /^\//)
})

// Retries until the links have these names, in this order
cy.getByRole('link').should(($links) => {
const names = $links.toArray().map((link) => link.textContent)

expect(names).to.deep.equal(['Profile', 'Billing', 'Security'])
})

Name​

Match by accessible name​

The accessible name comes from the element's content, a <label>, aria-label, aria-labelledby, an image's alt text, or a title attribute. A string must match the whole name, and it is case-sensitive. Whitespace in the name is collapsed to a single space and trimmed before it's compared.

index.html
<button>Save changes</button>
<button aria-label="Close dialog">×</button>
<label>Email <input type="email" /></label>
spec.cy.js
cy.getByRole('button', { name: 'Save changes' }).click()
cy.getByRole('button', { name: 'Close dialog' }).click()
cy.getByRole('textbox', { name: 'Email' }).type('[email protected]')

// Neither matches: a string matches the whole name, case-sensitively
cy.getByRole('button', { name: 'Save' }).should('not.exist')
cy.getByRole('button', { name: 'save changes' }).should('not.exist')

Content hidden with aria-hidden="true" is left out of the name. So is CSS ::before and ::after content, so that an icon-font glyph, which lives in a pseudo-element, doesn't become part of a button's name. Browsers include this content in the accessible name, so an element named only by ::before or ::after content has a name in the browser but not in cy.getByRole(). Match it with an aria-label or by another role and name instead.

index.html
<button>Send<span aria-hidden="true"> ✉️</span></button>
spec.cy.js
// Yields and clicks the <button>, whose accessible name is "Send" without the ✉️
cy.getByRole('button', { name: 'Send' }).click()

Match part of a name with a regular expression​

Pass a regular expression to match part of the name, or to ignore case.

index.html
<button>Delete account</button> <button>Delete draft</button>
spec.cy.js
// Yields both buttons, since each name starts with "Delete"
cy.getByRole('button', { name: /^delete/i }).should('have.length', 2)

// Yields and clicks only the "Delete draft" button
cy.getByRole('button', { name: /draft$/ }).click()

Match a name with a function​

A function receives the accessible name and the element, and returns true for a match.

index.html
<button>Pay $20.00</button> <button>Pay $120.00</button>
spec.cy.js
cy.getByRole('button', {
name: (name) => Number(name.replace(/[^\d.]/g, '')) > 100,
}).should('have.text', 'Pay $120.00')

Hidden elements​

Include elements hidden from the accessibility tree​

By default, cy.getByRole() skips an element that assistive technology can't reach:

  • The element or an ancestor has the hidden attribute, aria-hidden="true", or display: none.
  • The element's computed visibility is hidden. It inherits this from a visibility: hidden ancestor unless it sets visibility: visible itself.

Pass hidden: true to include them, such as to find a closed menu.

index.html
<nav aria-label="Account" hidden>
<a href="/settings">Settings</a>
</nav>
spec.cy.js
cy.getByRole('link', { name: 'Settings' }).should('not.exist')
cy.getByRole('link', { name: 'Settings', hidden: true }).should('exist')

An element that is only visually hidden, such as one clipped to a single pixel or set to opacity: 0, is still in the accessibility tree, so cy.getByRole() finds it without hidden: true. The hidden option is about what a screen reader can reach, not what Cypress considers visible.

Native elements​

Native elements by default​

A <div role="button"> has the button role, but it doesn't get a button's keyboard behavior unless the page adds it. For a role that HTML has an element for, such as button, link, heading, dialog, or checkbox, cy.getByRole() matches only elements whose tag gives them the role. The test fails if a native control is replaced with an imitation.

index.html
<button>Save</button>
<div role="button" tabindex="0">Cancel</div>
spec.cy.js
// 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')

For widget roles that HTML has no element for, such as tab, menuitem, or switch, a role attribute is the only way to give an element the role, so cy.getByRole() always matches the role attribute. Passing native: true with one of these roles throws an error.

Include elements with a role attribute​

Pass native: false to also match elements that have the role only through a role attribute, such as a <div role="dialog"> rendered by a component library.

spec.cy.js
// Yields the <div role="button">
cy.getByRole('button', { name: 'Cancel', native: false }).click()

Some widely used patterns need native: false even though they follow the ARIA authoring practices, because HTML has a different 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>.

When an element was skipped only because it has the role through a role attribute, the error says so and suggests passing { native: false }.

Change the native default for your project​

If your project relies on a component library that re-implements browser roles with role attributes, such as a <div role="button">, you can make native: false the default. Overwrite the query with Cypress.Commands.overwriteQuery() in your support file. A test can still pass native: true to require a native element.

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

Scope​

Search inside another element​

Chained off a command that yields DOM elements, cy.getByRole() searches the descendants of each of them. It never matches the subject element(s) it is chained off.

index.html
<header>
<nav aria-label="Main">
<a href="/">Home</a>
</nav>
</header>
<footer>
<a href="/privacy">Privacy</a>
</footer>
spec.cy.js
cy.getByRole('navigation', { name: 'Main' })
.getByRole('link')
.should('have.length', 1)
.and('have.text', 'Home')

Act inside a dialog with .within()​

Inside a .within() callback, cy.getByRole() searches only the .within() subject.

index.html
<button>Delete</button>
<dialog open aria-label="Delete user">
<button>Cancel</button>
<button>Delete</button>
</dialog>
spec.cy.js
cy.getByRole('dialog', { name: 'Delete user' }).within(() => {
cy.getByRole('button', { name: 'Delete' }).click()
})

Narrow the results by state with .filter()​

cy.getByRole() matches on role and name only. To narrow the results by a state or property, chain .filter(), or chain .not() to leave matches out.

// 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"]')

Waiting for elements​

Wait for an element to appear​

cy.getByRole() retries until a matching element exists and every chained assertion passes, so it waits for an alert that renders after a save.

cy.getByRole('button', { name: 'Save' }).click()
cy.getByRole('alert').should('have.text', 'Changes saved')

Assert an element doesn't exist​

cy.getByRole() retries a negative assertion too, until the element is gone.

cy.getByRole('dialog').should('not.exist')

Options​

Find elements inside shadow DOM with includeShadowDom​

By default, cy.getByRole() doesn't search inside shadow roots. Pass includeShadowDom: true to search them, including nested shadow roots, or set the includeShadowDom configuration option to search them in every query.

cy.getByRole('button', { name: 'Checkout', includeShadowDom: true }).click()

To search one shadow root only, chain cy.getByRole() off .shadow():

cy.get('checkout-panel').shadow().getByRole('button', { name: 'Checkout' })

TypeScript​

Type the yielded element​

The role argument autocompletes role names, and its type allows any other string. At runtime, a role that contains a space still throws an error. Pass a type argument to type the yielded element.

cy.getByRole<HTMLInputElement>('textbox', { name: 'Email' }).then(($input) => {
// $input is typed as JQuery<HTMLInputElement>
expect($input[0].value).to.eq('')
})

Notes​

What a cy.getByRole() error looks like​

When no element matches before the timeout, the error says what it looked for and lists the roles and accessible names it did find. Here, the name was misspelled:

Timed out retrying after 4000ms: Expected to find an accessible native element with the role "button" and name "Svae", but never did.

Here are the accessible roles that were found, with the accessible name of each element:

- main: (no name)
- heading: "Account settings"
- button: "Save", "Cancel"

How cy.getByRole() decides an element's role​

cy.getByRole() maps elements to roles with aria-query. Some roles depend on attributes: an <a> needs an href to be a link, a <section> needs an aria-label or aria-labelledby to be a region, and an <input> with a missing or unknown type is a textbox.

Some roles depend on the surrounding table: a <td> is a cell in a table, a gridcell in a grid or treegrid, and has no role in a table with role="presentation". Otherwise, the mapping doesn't consider where an element sits in the page, so every <header> is a banner and every <footer> a contentinfo, even inside an <article>, where a browser gives them no landmark role.

Rename an existing getByRole custom command​

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

How to migrate from Testing Library findByRole() to cy.getByRole()​

cy.getByRole() does what the findByRole() and findAllByRole() queries from Cypress Testing Library do for role queries, without a plugin. Most calls translate by renaming the query, and the state options become a chained .filter().

Before: Cypress Testing Library
spec.cy.js
cy.findByRole('button', { name: 'Save' }).click()
cy.findByRole('button', { name: /save/i }).click()
cy.findAllByRole('listitem').should('have.length', 3)
cy.findByRole('tab', { selected: true }).should('have.text', 'Billing')
cy.findByRole('checkbox', { checked: true }).should(
'have.attr',
'name',
'terms'
)
After: cy.getByRole()
spec.cy.js
cy.getByRole('button', { name: 'Save' }).click()
cy.getByRole('button', { name: /save/i }).click()
cy.getByRole('listitem').should('have.length', 3)
cy.getByRole('tab')
.filter('[aria-selected="true"]')
.should('have.text', 'Billing')
cy.getByRole('checkbox').filter(':checked').should('have.attr', 'name', 'terms')

Keep these differences in mind as you migrate:

  • Native elements only, by default. For a role that HTML has an element for, cy.getByRole() skips elements that have the role only through a role attribute, so a test that found a <div role="button"> with findByRole() fails after the migration. Decide for each one whether the application should use the native element, or whether the test should pass native: false. To keep your current behavior while you migrate, overwrite the query to default to native: false, or wrap it in a custom query of your own.
  • Every match, not one. cy.getByRole() yields every match, like findAllByRole(), instead of failing when more than one element matches. Narrow the name, or chain .first(), to act on one.
  • State options. The checked, selected, pressed, expanded, current, busy, level, and value options become a chained .filter(). The error for each of these options names the filter to use.

When no other Testing Library queries remain in your tests, remove @testing-library/cypress from your dependencies, its import from your support file, and its entry in the types of your tsconfig.json.

Migrate from Testing Library findByRole() to cy.getByRole()

Move your role queries to the built-in cy.getByRole() and get a list of the elements that need a decision about native elements.

Migrate this project's Cypress tests from Testing Library's findByRole() and findAllByRole() queries to the built-in cy.getByRole() query. First read https://docs.cypress.io/api/commands/getbyrole.md, then work through these steps: 1. Find every cy.findByRole() and cy.findAllByRole() call in the Cypress specs, custom commands, and support files. If the project defines its own getByRole custom command, stop and tell me, since it conflicts with the built-in query. 2. Replace each call with cy.getByRole(), keeping the role, the command it's chained from, and the name, timeout, log, and hidden options as they are. 3. Replace the checked, selected, pressed, expanded, current, busy, level, and value options with a chained .filter(), as the page describes. Use :checked only for native checkboxes and radios, and [aria-checked="true"] for elements that get their role from a role attribute. 4. Run the migrated specs. cy.getByRole() matches only native elements for roles that HTML has an element for, so a test that found an element such as a <div role="button"> now fails. Don't add native: false to every call. List each of these elements with its spec file and line, so I can decide whether the application should use the native element or the test should pass native: false. 5. If no other Testing Library queries remain, remove @testing-library/cypress from package.json, its import from the support file, and its entry in the types of tsconfig.json. Keep it if any other Testing Library query, such as findByLabelText(), is still in use. 6. Flag any option or pattern with no direct equivalent instead of guessing. Finish with a summary of what changed and the list of elements that need a decision.

Rules​

Requirements ​

  • cy.getByRole() can be chained off of cy or off a command that yields DOM element(s). Chained off cy.window() or cy.document(), it searches the whole page.

Assertions ​

  • cy.getByRole() automatically retries until the element(s) exist in the DOM.
  • cy.getByRole() automatically retries until all chained assertions have passed.

Timeouts ​

  • cy.getByRole() can time out waiting for the element(s) to exist in the DOM.
  • cy.getByRole() can time out waiting for assertions you've added to pass.

Command Log​

Find a button by its role and name

cy.getByRole('button', { name: 'Save changes' }).click()

The Command Log shows a getByRole row with the role and options, followed by the click. When clicking on the getByRole command within the command log, the console outputs the role, the options, the element the query was applied to, the yielded element(s), and the number of elements found.

History​

VersionChanges
16.2.0cy.getByRole() command added

See also​