Tabindex: it rarely pays to be positive
HTML’s tabindex
attribute may be used to modify whether an element can receive keyboard focus or not. It’s an attribute that can be quite beneficial in creating custom widgets, and ensuring elements that should be able to receive keyboard focus in some way are keyboard accessible. It can also be easily abused by people who are often trying to do the right thing, but don’t quite understand the undesired impact the attribute can have when misused.
Quick summary of tabindex
usage
One of the things people have to be incredibly careful about when using tabindex
, is knowing exactly when to use it. Accepting a value of 0
, or positive or negative integers, the attribute will produce the following effects:
Negative values
tabindex="-1"
can be used to remove an otherwise focusable element from the document’s tab order. One might do this to remove redundant controls. E.g., a carousel with scroll markers to navigate to each panel of content is presented with previous and next buttons. These are redundant since they perform the same action, so to reduce extra tab stops - it’s common to remove either the scroll markers, or the prev/next buttons from the tab order with a negative tabindex
.
The attribute can also be used to allow focus to be programmatically set to an element that would otherwise not be able to receive focus, but also should not be in the document’s tab order. For example, a heading that is the target of a skip link.
The value of the negative integer makes no difference when declared. For instance, tabindex="-1"
and tabindex="-100"
work the same. I’ve never gotten a logical response to why someone would set negative integers beyond -1
. Sometimes people just do things…
Zero value
tabindex="0"
will allow an element to become focusable and be included in the document’s sequential tab order. HTML elements that natively receives keyboard focus (button
, a href="..."
, summary
, form controls, etc.) do not require a tabindex="0"
to be declared. You don’t need to do <button tabindex=0>
. If you feel like I was speaking directly to you with that example, then I was.
Where a tabindex="0"
would be appropriate is if creating a custom element or widget using ARIA attributes. For instance, <span role="button">
. A span
element is not natively focusable, but ARIA’s role="button"
has rewritten the role semantics of the span
so that it is exposed to the browser’s accessibility tree as a “button”. For the majority of use cases for a general button, when it is not in the disabled state it should be able to receive keyboard focus via the Tab key. So a custom ARIA button would similarly need to be keyboard focusable in this manner, so add the tabindex=0
.
<span role="button" tabindex="0">
Don't forget to write JavaScript
that allows me to be activated by
Space and Enter keys as well!
Also don't forget to give me
appropriate high contrast styling.
Also, maybe just use a button
element and make your job easier?
</span>
Positive value
Outside of very specific corner cases, a tabindex
should not be given a positive integer value. People who use keyboards to interact with the web expect a web page to be navigable in sequential order, starting at the top left (or right depending on language of the document) and going in order from there. Messing around with this expectation can land you in trouble with WCAG 2.4.3: Focus Order pretty quickly.
Consider the following markup:
<nav>
<ul>
<li><a href="...">Page 1</a>
<!-- ... -->
<li><a href="...">Page 6</a>
</ul>
</nav>
<main>
<!-- ... -->
<p>
...
<a href="..." tabindex="1">go somewhere</a>
...
</p>
</main>
A user navigating by Tab key from the browser’s address bar into the document would expect to go to the “Page 1” link. However, due to the tabindex="1"
on the link within the main
element of the page, keyboard focus would instead unexpectedly shoot past all the focusable elements prior to that link. Upon subsequent Tab key presses, focus would move to any other element with a positive tabindex
in the document, before finally returning to the top of the document to begin focusing natively focusable elements, or elements with tabindex="0"
. Good job, we’ve made the keyboard navigation experience a whack-a-mole experience of “where the heck am I going? I can see my destination… why can’t I logically get there? Why do these people hate me?”
Don’t hate your users. Be nice.
Managing positive tabindex
values in a single document, let alone across an entire website or application, would be a ridiculous level of effort. Don’t take on a ridiculous level of effort. Relax.
A corner case in the shadows?
While a positive tabindex
should typically be avoided, there may be some use for it when building custom component and using the Shadow DOM. Exactly what that use is, I can only speculate… But, the reason I even mention this is because positive tabindex
values on elements within the Shadow DOM, do not have an impact on the light DOM’s document tabbing order.
For example, the first and last of the following four buttons are in the standard document (light DOM) and the two in between are in the Shadow DOM. One of the Shadow DOM buttons has a tabindex="1"
. If you keyboard tab through the document, keyboard focus will not immediately go to the tabindex="1"
button until keyboard focus navigates into the Shadow DOM.
Shadow Buttons:
See the code
Here is the code for the quick and dirty custom element that contains the two buttons.
<p>
<button>In the light</button>
</p>
<p>
Shadow Buttons:<br>
<positive-btns></positive-btns>
</p>
<p>
<button>In the light again</button>
</p>
<script>
class pBtns extends HTMLElement {
constructor () {
super();
const doc = document;
// Create a shadow root
const shadow = this.attachShadow({
mode: 'open'
});
const styles = document.createElement('style');
defineStyles( styles );
shadow.appendChild(styles);
const btn = document.createElement('button');
const btn2 = document.createElement('button');
btn2.tabIndex = '1';
btn.textContent = 'First in DOM';
btn2.textContent = 'First in Tab Order';
// Append elements to the shadow root.
shadow.appendChild(btn);
shadow.appendChild(btn2);
}
}
/**
* Just wanted to move this out of the
* constructor...
*/
const defineStyles = function ( styles ) {
styles.textContent = ''+
'button {' +
'-webkit-appearance: button;' +
'font-family: inherit;' +
'font-size: 100%;' +
'line-height: 1.15;' +
'margin: 0;' +
'overflow: visible;' +
'text-transform: none;' +
'}' +
'button::-moz-focus-inner {' +
'border-style: none;' +
'padding: 0;' +
'}' +
'button:-moz-focusring {' +
'outline: 1px dotted ButtonText;' +
'}'
}
// define the custom element
customElements.define('positive-btns', pBtns);
</script>
What is the benefit of using a positive tabindex
here, rather than appending the buttons in the correct document order? Really none outside of showing that the primary document’s tab order doesn’t get negatively affected by positive tabindex
values in the Shadow DOM.
But maybe knowing this may help you out in some situation I haven’t thought of yet. Or maybe (likely) this is still just a horrible no good very bad idea.
In which case, ignore please. Just write your custom component to have a logical tab order, and don’t get clever. We don’t need clever, we just need good UX.