Accessibility: Why we disallow role=”button”

Bharat Gupta
smallcase Engineering
3 min readAug 7, 2022

At smallcase, we are constantly working on making our web applications more accessible. As part of these efforts, we have come across patterns where we consistently see developers getting confused or making mistakes. One of such repeated issues has led us to enforce the following rule: We do not allow passing role = button in non-semantic HTML elements(like div, section, span) instead we want the devs to use a button element that is styled as per requirements.

This blog talks about why we enforced this rule, and the solution using the button element.

Why?

We do not allow passing role=” button” because after passing role as a button it becomes mandatory to pass onKeyDown and tabIndex properties to make the element accessible. If you are using eslint with the accessibility plugin, it will remind you to pass both of these, but passing these properties manually is prone to accessibility problems if not passed carefully.

Problems:

onKeyDown: The onKeyDown function passed to element is called when a button on the keyboard is pressed while the element is focused. The problem happens when the developer forgets to add a check for which key to trigger the onKeyDown causing the onKeyDown function to be called on the press of any key(eg. Tab key) when the user is accessing the website using the keyboard only.

tabIndex: The tabIndex makes a DOM element focusable and allows/prevent them from being sequentially focusable. Passing a wrong value to tabIndex can mess up the order of tab accessibility. Determining what value to pass to tabIndex is usually complicated and should be done with caution.

As an example, below is a video of one of the prod issues we faced when one developer incorrectly specified onKeyDown without filtering for the space/enter key.

Prod bug we faced:

In the above example, we are accessing the page using the keyboard(tab button) and the methodology link is a div with onKeyDown and tabIndex manually passed for accessibility but we missed checking for which key to trigger onKeyDown. Therefore, when the methodology link is focused and then the tab is pressed, the modal opens, along with tabbing to the next item.

Solution:

A better solution is to use the button element instead div because the button takes care of onKeyDown and tabIndex prop automatically so that developers don’t have to worry about them. We usually reset the styles of buttons and design them according to the requirement using CSS.

How to reset button styles?

Stylesheet:

.reset-button-styles {
padding: 0;
margin: 0;
background: none;
border: none;
outline: none;
}
.primary-cta {
font-size: 16px;
font-weight: bold;
color: #2f363f;
cursor: pointer
}
.primary-cta:hover {
color: #1f7ae0;
}

Clickable Div(not allowed):

<div class="primary-cta" onPress={() => {}} onKeyDown={() => {}} tabIndex={0}>Call to Action</div>

Button(allowed):

<button class="reset-button-styles primary-cta" onPress={() => {}}>Call to Action</button>

In the above code snippets, to start using <button/> instead of <div/> we started passing reset-button-styles (to reset default button styles) and primary-cta(to add custom styles).

Please note that in your codebase, resetting the button styles and overriding them to make them look alright might require more CSS than mentioned in the above snippet.

Next steps:

  • Write a custom eslint rule that enforces this convention
  • May be create a unstyled button component which has css defaults like a div, and make sure devs use that instead of using a div with onClick

--

--

Bharat Gupta
smallcase Engineering

Software Engineer. React | React Native | Next.js | PHP | Laravel