Many forms of assistive technology use keyboard navigation to understand and take action on screen content. One way of navigating is via the Tab key. You may already be familiar with this way of navigating if you use it to quickly jump from input to input on a form without having to reach for your mouse or trackpad.
Tab will jump to interactive elements in the order they show up in the DOM. This is one of the reasons why it is so important that the order of your source code matches the visual hierarchy of your design.
The list of interactive elements that are tabbable is:
- Anchors, when the
href
attribute is present, <button>
,<input>
and<textarea>
, with an accompanying label,<select>
,<details>
,<audio>
and<video>
when controls are present,<object>
, depending on how it is used,- any element with scroll overflow in Firefox,
- any element with the
contenteditable
attribute applied to it, and - any element with the
tabindex
attribute applied to it (more on this one in a bit).
An interactive element gains focus when:
- It has been navigated to via the Tab key,
- it is clicked on, following an anchor that links to another focusable element,
- or focus is programmatically set through
element.focus()
in JavaScript.
Focus is analogous to hovering over an element with your mouse cursor, in that you’re identifying the thing you want to activate. It’s also why visually obvious focus styles are so important.
Focus management
Focus management is the practice of coordinating what can and cannot receive focus events. It is one of the trickier things to do in front-end development, but it is important for making websites and web apps accessible.
Good practices for focus management
99% of the time, you want to leave focus order alone. I cannot stress this enough.
Focus will just work for you with no additional effort required, provided you’re using the <button>
element for buttons, the anchor element for links, the <input>
element for user input, etc.
There are rare cases where you might want to apply focus to something out of focus order, or make something that typically can’t receive focus events be focusable. Here are some guidelines for how to go about it in an accessible, intuitive to navigate way:
✅ Do: learn about the tabindex
attribute
tabindex
allows an element to be focused. It accepts an integer as a value. Its behavior changes depending on what integer is used.
❌ Don’t: Apply tabindex="0"
to things that don’t need it
Interactive elements that can receive keyboard focus (such as the <button>
element) don’t need to have the tabindex
attribute applied to them.
Additionally, you don’t need to declare tabindex
on non-interactive elements to ensure that they can be read by assistive technology (in fact, this is a WCAG failure if no role and accessible name is present). Doing so actually creates an unexpected and difficult to navigate experience for a person who uses assistive technology — they have other, expected ways to read this content.
tabindex="-1"
for focusing with JavaScript
✅ Do: Use tabindex="-1"
is used to create accessible interactive widgets with JavaScript.
A declaration of tabindex="-1"
will make an element focusable via JavaScript or click/tap. It will not, however, let it be navigated to via the Tab key.
tabindex
value
❌ Don’t: Use a positive integer as a This is a serious antipattern. Using a positive integer will override the expected tab order, and create a confusing and disorienting experience for the person trying to navigate your content.
One instance of this is bad enough. Multiple declarations is a total nightmare. Seriously: don’t do it.
❌ Don’t: Create a manual focus order
Interactive elements can be tabbed to just by virtue of being used. You don’t need to set a series of tabindex
attributes with incrementing values on every interactive element in the order you think the person navigating your site should use. You’ll let the order of the elements in the DOM do this for you instead.
Focus trapping
There may be times where you need to prevent things from being focused. A good example of this is focus trapping, which is the act of conditionally restricting focus events to an element and its children.
Focus trapping is not to be confused with keyboard traps (sometimes referred to as focus traps). Keyboard traps are situations where someone navigating via keyboard cannot escape out of a widget or component because of a nasty loop of poorly-written logic.
A practical example of what you would use focus trapping for would be for a modal:
Why is it important?
Keeping focus within a modal communicates its bounds, and helps inform what is and is not modal content — it is analogous to how a sighted person can see how a modal “floats” over other website or web app content. This is important information if:
- You have low or no vision and rely on screen reader announcements to help communicate the shift in interaction mode.
- You have low vision and a magnified display, where focusing outside of the bounds of the modal may be confusing and disorienting.
- You navigate solely via keyboard and could otherwise tab out of the modal and get lost on the underlying page or view trying to get back into the modal.
How do you do it?
Reliably managing focus is a complicated affair. You need to use JavaScript to:
- Determine the container elements of all focusable elements on the current page or view.
- Identify the bounds of the trapped content, including the first and last focusable item.
- Remove both interactivity and discoverability from anything identified as focusable that isn’t within that set of trapped content.
- Move focus into the trapped content.
- Listen for events that signals dismissing the trapped content (save, cancel, dismissal/hitting the Esc key, etc.).
- Dismiss the trapped content area when triggered by a pertinent event.
- Restore previously removed interactivity.
- Move focus back to the interactive element that triggered the trapped content.
Why do we do it?
I’m not going to lie: this is all tricky and time-consuming to do. However, focus management and a sensible, usable focus order is a Web Content Accessibility Guideline. It’s important enough that it’s considered part of an international, legally-binding standard about usability.
Tabbable and discoverable
There’s a bit of a trick to removing both discoverability and interactivity.
Screen readers have an interaction mode that allows them to explore the page or view via a virtual cursor. The virtual cursor also lets the person using the screen reader discover non-interactive parts of the page (headings, lists, etc.). Unlike using Tab and focus styles, the virtual cursor is only available to people using a screen reader.
When you are managing focus, you may want to restrict the ability for the virtual cursor to discover content. For our modal example, this means preventing someone from accidentally “breaking out” of the bounds of the modal when they’re reading it.
Discoverability can be suppressed via a judicious application of aria-hidden="true"
. However, interactivity is a little more nuanced.
inert
Enter The inert
attribute is a global HTML attribute that would make removing, then restoring the ability of interactive elements to be discovered and focused a lot easier. Here’s an example of how it would work:
<body>
<div
aria-labelledby="modal-title"
class="c-modal"
id="modal"
role="dialog"
tabindex="-1">
<div role="document">
<h2 id="modal-title">Save changes?</h2>
<p>The changes you have made will be lost if you do not save them.<p>
<button type="button">Save</button>
<button type="button">Discard</button>
</div>
</div>
<main inert>
<!-- ... -->
</main>
</body>
I am deliberately avoiding using the <dialog>
element for the modal due to its many assistive technology support issues.
inert
has been declared on the <main>
element following a save confirmation modal. What this means that all content contained within <main>
cannot receive focus nor be clicked.
Focus is restricted to inside of the modal. When the modal is dismissed, inert
can be removed from the <main>
element. This way of handling focus trapping is far easier compared to existing techniques.
Remember: A dismissal event can be caused by the two buttons inside our modal example, but also by pressing Esc on your keyboard. Some modals also let you click outside of the modal area to dismiss, as well.
Support for inert
The latest versions of Edge, Chrome, and Opera all support inert
when experimental web platform features are enabled. Firefox support will also be landing soon! The one outlier is both desktop and mobile versions of Safari.
I’d love to see Apple implement native support for inert
. While a polyfill is available, it has non-trivial support issues for all the major screen readers. Not great!
In addition, I’d like to call attention to this note from the inert
polyfill project’s README:
The polyfill will be expensive, performance-wise, compared to a native
inert
implementation, because it requires a fair amount of tree-walking.
Tree-walking means the JavaScript in the polyfill will potentially require a lot of computational power to work, and therefore slow down the end-user experience.
For lower power devices, such as budget Android smartphones, older laptops, and more powerful devices doing computationally-intensive tasks (such as running multiple Electron apps), this might mean freezing or crashing occurs. Native browser support means this sort of behavior is a lot less taxing on the device, as it has access to parts of the browser that JavaScript doesn’t.
Safari
Personally, I am disappointed by Apple’s lack of support for inert
. While I understand that adding new features to a browser is incredibly complicated and difficult work, inert
seems like a feature Apple would have supported much earlier.
macOS and iOS have historically had great support for accessibility, and assistive technology-friendly features are a common part of their marketing campaigns. Supporting inert
seems like a natural extension of Apple’s mission, as the feature itself would do a ton for making accessible web experiences easier to develop.
Frustratingly, Apple is also tight-lipped about what it is working on, and when we can generally expect to see it. Because of this, the future of inert
is an open question.
Igalia
Igalia is a company that works on browser features. They currently have an experiment where the public can vote on what features they’d like to see. The reasoning for this initiative is outside the scope of this article, but you can read more about it on Smashing Magazine.
One feature Igalia is considering is adding WebKit support for inert
. If you have been looking for a way to help improve accessibility on the web, but have been unsure of how to start, I encourage you to pledge. $5, $10, $25. It doesn’t have to be a huge amount, every little bit adds up.
Unfortunately, inert
did not win the Open Prioritization experiment. This means that we are back to not knowing if Apple is working on it, or when we can expect to see it showing up in Safari Technology Preview.
iOS 15.4 will be shipping with support for the inert
attribute disabled. This is amazing news, as it signals Apple is working on it.
Wrapping up
Managing focus requires some skill and care, but is very much worth doing. The inert
attribute can go a long way to making this easier to do.
Technologies like inert
also represents one of the greatest strengths of the web platform: the ability to pave the cowpaths of emergent behavior and codify it into something easy and effective.
Further reading
- Controlling focus with tabindex (A11ycasts, Episode 04)
- Using the tabindex attribute (The Paciello Group)
- Using JavaScript to trap focus in an element (Hidde de Vries)
Thank you to Adrian Roselli and Sarah Higley for their feedback.
I don’t think we should encourage Igalia and its initiative.
Apple is by far the worst offender for browser features support and it’s the richest company on earth… We don’t need to pay for someone else to add a feature to Safari. Apple needs to get off their lazy rear end and actually do the job.
As for the most popular browser, Chrome, they’re actually pushing a lot of new standards, which is good. But we certainly don’t need to pay extra there either. They’re definitely at the top end of the food chain for market capitalization, they’re filthy rich enough…
Edge is great, the move to Chromium helped them a lot, if not a bit unfortunate for web standards. But again, it’s not like Microsoft needs our money, they’re at the top of food chain too, very close to Apple.
If you really want to encourage the web, encourage Firefox. Google bled them dry by hiring some of their engineers, then built their own browser while cutting a large part of their funding to Firefox…
I’d be fine if Igalia weren’t working on browsers made by 1+ Trillion dollars companies… But as it is right now, I can’t find any reason to help them in any way.
Totally agree.
And a +1 from me for Firefox. I’ve used it since it first came out, and it’s still my preferred browser.
Hello Eric, thank you for this presentation.
Do you know why adding
inert
on each element to momentarily mark as undetectable and non-interactive, was preferred over adding it a single attribute to the element to (momentarily) mark as the only element of the document that should be detectable and interactive?As an alternative to a feature like
inert
, which is not yet well supported across browsers, it’s relatively easy to trap focus by installing akeydown
handler on the widget container element (e.g., the dialogdiv
in the example), and handle the Tab (and Shift-Tab!) key. Thetarget
property of the event indicates which element had focus when the keypress occurred. The event handler only needs to know which interactive elements are “first” and “last” in the tab order within the widget, and when one of these elements is thetarget
, useelement.focus()
to move the focus to the appropriate “next” element; otherwise, do nothing and allow the event to propagate.Since most complex widgets that require focus handling will have other key handling needs, it’s not much more work to include Tab handling as well. And doing so limits the focus handling to just the elements within the widget, rather than needing to alter the DOM outside of the widget.
What about the virtual cursor in a screen reader? Would a keydown handler that, too?
Another way to implement focus trapping is to use 2 trappers as the first and the last tabbable elements within the trapping content container, e.g.:
In JS:
I would have preferred an opt-in approach to focus trapping, such as which, when the
modal
attribute is present, traps focus within the containing element.From the perspective of a component author, how will a dialog know what element(s) should become
inert
when the dialog is activated? It can’t. But it could attachmodal
to itself and solve the problem quite elegantly.