Roving tabindex 0.3.0
GitHubA simple HTML web component that implements the roving tabindex pattern for building accessible menus and grids.
Wrap it around some markup and give it a selector to determine which elements become navigable!
<roving-tabindex selector="button">
<ul class="menu" role="toolbar" aria-label="toolbar example">
<li>
<button>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
<use href="#cut" />
</svg>
<span>Cut</span>
</button>
</li>
<li>
<button>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
<use href="#copy" />
</svg>
<span>Copy</span>
</button>
</li>
<li>
<button>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
<use href="#paste" />
</svg>
<span>Paste</span>
</button>
</li>
</ul>
</roving-tabindex>
(Note: these docs are out of date pending a rewrite)
Usage
<roving-tabindex>
isn't on NPM; instead, you install it by downloading
roving-tabindex.js
and adding it to your codebase. The default export is a class with a static
define
method:
<script type="module">
import RovingTabindex from "/roving-tabindex.js";
RovingTabIndex.define();
<script/>
By default, it defines the tag name <roving-tabindex>
. You can customize
it by passing a different string:
<script type="module">
import RovingTabindex from "/roving-tabindex.js";
RovingTabIndex.define("other-tagname");
<script/>
Once you've defined the tag, wrap <roving-tabindex>
(or whatever name you
chose) around the markup for which you want arrow key navigation. Use the
selector
prop to determine which descendant elements should be included.
<roving-tabindex selector="button, a" direction="horizontal">
<ul class="menu" role="toolbar" aria-label="toolbar-example">
<li><button>One</button></li>
<li><button>Two</button></li>
<li><button>Three</button></li>
<li><a href="#">Link</a></li>
</ul>
</roving-tabindex>
Make sure you set
WAI-ARIA-related properties
as appropriate. For example, the previous demo implements keyboard navigation described in
the toolbar pattern, but
<roving-tabindex>
doesn't manage the role
and
aria-label
attributes.
Examples
Menu Bar
The simplest use of <roving-tabindex>
is to make a one dimensional list
of buttons or links navigable with arrow keys.
By default, the left/up and right/down arrow keys move forward and backward in the list,
respectively, but you can set the direction
attribute to
"horizontal"
or "vertical"
to use only one dimension.
Source
<roving-tabindex selector="button, a" direction="horizontal">
<ul class="menu" role="toolbar" aria-label="toolbar-example">
<li><button>One</button></li>
<li><button>Two</button></li>
<li><button>Three</button></li>
<li><a href="#">Link</a></li>
</ul>
</roving-tabindex>
This works especially well as ✨ progressive enhancement
✨ on menus that are already keyboard navigable with the tab key.
For instance, here's this example without <roving-tabindex>
. The arrow
keys don't work, but you can still access each menu item by tabbing through.
Spreadsheet
<roving-tabindex>
can also be used to navigate in two dimensions by
setting the direction
attribute to "grid"
and setting the
columns
attribute to the number of columns. Here, it's used to replicate a
spreadsheet interface.
Each table cell contains a <label>
and a hidden
<input>
. When the user presses a key other than the keys
<roving-tabindex>
uses for navigation on a <label>
,
hide it and show the corresponding <input>
. When the user presses
Enter
or Escape
on the <input>
, swap it back.
Call stopPropagation
on keydown
events originating from
<input>
elements to prevent <roving-tabindex>
from
changing the focused element while editing text.
A | B | C | D | E | |
---|---|---|---|---|---|
1 | |||||
2 | |||||
3 | |||||
4 | |||||
5 |
Source
<roving-tabindex selector="label" direction="grid" columns="5">
<table id="spreadsheet">
<thead>
<tr>
<th width="0"></th>
<th width="20%">A</th>
<th width="20%">B</th>
<th width="20%">C</th>
<th width="20%">D</th>
<th width="20%">E</th>
</tr>
</thead>
<tbody>
<tr>
<th>1</th>
<td><label for="a1"></label><input class="hidden" id="a1" /></td>
<td><label for="a2"></label><input class="hidden" id="a2" /></td>
<td><label for="a3"></label><input class="hidden" id="a3" /></td>
<td><label for="a4"></label><input class="hidden" id="a4" /></td>
<td><label for="a5"></label><input class="hidden" id="a5" /></td>
</tr>
<tr>
<th>2</th>
<td><label for="b1"></label><input class="hidden" id="b1" /></td>
<td><label for="b2"></label><input class="hidden" id="b2" /></td>
<td><label for="b3"></label><input class="hidden" id="b3" /></td>
<td><label for="b4"></label><input class="hidden" id="b4" /></td>
<td><label for="b5"></label><input class="hidden" id="b5" /></td>
</tr>
<tr>
<th>3</th>
<td><label for="c1"></label><input class="hidden" id="c1" /></td>
<td><label for="c2"></label><input class="hidden" id="c2" /></td>
<td><label for="c3"></label><input class="hidden" id="c3" /></td>
<td><label for="c4"></label><input class="hidden" id="c4" /></td>
<td><label for="c5"></label><input class="hidden" id="c5" /></td>
</tr>
<tr>
<th>4</th>
<td><label for="d1"></label><input class="hidden" id="d1" /></td>
<td><label for="d2"></label><input class="hidden" id="d2" /></td>
<td><label for="d3"></label><input class="hidden" id="d3" /></td>
<td><label for="d4"></label><input class="hidden" id="d4" /></td>
<td><label for="d5"></label><input class="hidden" id="d5" /></td>
</tr>
<tr>
<th>5</th>
<td><label for="a1"></label><input class="hidden" id="e1" /></td>
<td><label for="a2"></label><input class="hidden" id="e2" /></td>
<td><label for="a3"></label><input class="hidden" id="e3" /></td>
<td><label for="a4"></label><input class="hidden" id="e4" /></td>
<td><label for="a5"></label><input class="hidden" id="e5" /></td>
</tr>
</tbody>
</table>
</roving-tabindex>
<script>
spreadsheet.addEventListener("keydown", evt => {
if (evt.target instanceof HTMLLabelElement) {
switch (evt.key) {
// send arrow keys and home/end through to <roving-tabindex>
case "ArrowUp":
case "ArrowDown":
case "ArrowLeft":
case "ArrowRight":
case "Home":
case "End":
break;
// for all other keys, swap in the corresponding <input> element
default:
const input = spreadsheet.querySelector("#" + evt.target.htmlFor);
evt.target.classList.add("hidden");
input.classList.remove("hidden");
input.focus();
break;
}
}
if (evt.target instanceof HTMLInputElement) {
// stop keydown events on <input> elements from reaching <roving-tabindex>
evt.stopPropagation();
const label = spreadsheet.querySelector(`[for="${evt.target.id}"]`);
switch (evt.key) {
case "Enter":
label.innerText = evt.target.value;
// fallthrough
case "Escape":
evt.target.classList.add("hidden");
label.classList.remove("hidden");
label.focus();
break;
}
}
});
spreadsheet.addEventListener("focusout", evt => {
if (evt.target instanceof HTMLInputElement) {
const label = spreadsheet.querySelector(`[for="${evt.target.id}"]`);
label.innerText = evt.target.value;
evt.target.classList.add("hidden");
label.classList.remove("hidden");
}
});
</script>
API
There are three main ways to control <roving-tabindex>
and the focus of
the elements it manages: attributes,
methods and events.
Attributes
Attributes are applied to the <roving-tabindex>
tag in HTML to control
its behavior.
Name | Description | Default |
---|---|---|
selector |
A CSS selector that determines which descendant elements should be navigable. | |
direction |
Determines the behavior of the arrow keys.
|
"both" |
columns |
The number of columns each grid row has. | 0 |
loop |
Whether to wrap around to the beginning of the selection when moving past the end, and vice versa. | false |
Methods
<roving-tabindex>
provides methods that can be called with a direct
reference to its DOM node.
Name | Description |
---|---|
focus() |
Return focus to the most recently focused descendant. |
Events
The focus of elements can also be controlled by dispatching custom events, either directly
on the
<roving-tabindex>
element itself or on one of its descendants.
Name | Description | Detail |
---|---|---|
rove |
Move focus by a given number of rows or columns. | { rows?: number; cols?: number } |