v146 · HTML · Accessibility · demo
Before / After: Roving Tabindex
The same accessible radio group — both keyboard-navigable with ↑ ↓ arrows — implemented two ways. The old way needs 40+ lines of JavaScript. The new way needs one HTML attribute.
❌ Before — manual roving tabindex
~40 JS lines
Newest first
Oldest first
Most popular
Alphabetical
Focus a radio option, then remove it to test the mutation edge case.
// Roving tabindex implementation
const rg = document.getElementById('rg-old');
const items = [...rg.querySelectorAll('[role=radio]')];
rg.addEventListener('keydown', e => {
const idx = items.indexOf(document.activeElement);
if (idx === -1) return;
let next = -1;
if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
next = (idx + 1) % items.length;
} else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') {
next = (idx - 1 + items.length) % items.length;
} else if (e.key === 'Home') {
next = 0;
} else if (e.key === 'End') {
next = items.length - 1;
}
if (next !== -1) {
e.preventDefault();
// Remove old active tabindex
items.forEach(el => {
el.setAttribute('tabindex', '-1');
el.setAttribute('aria-checked', 'false');
});
// Set new active item
items[next].setAttribute('tabindex', '0');
items[next].setAttribute('aria-checked', 'true');
items[next].focus();
}
});
// Also handle click
items.forEach((item, i) => {
item.addEventListener('click', () => {
items.forEach(el => {
el.setAttribute('tabindex', '-1');
el.setAttribute('aria-checked', 'false');
});
items[i].setAttribute('tabindex', '0');
items[i].setAttribute('aria-checked', 'true');
});
});
Old: ~40 lines of JS per widget, plus mutation repair if you need it.
✅ After — focusgroup
1 HTML attr
Newest first
Oldest first
Most popular
Alphabetical
The native focusgroup contract should promote focus when the active item disappears; the test button simulates that expected recovery when native support is absent.
<!-- The entire keyboard navigation is declared here -->
<div role="radiogroup"
focusgroup="radiogroup block wrap">
<div role="radio" aria-checked="true">
Newest first
</div>
<div role="radio" aria-checked="false">
Oldest first
</div>
<div role="radio" aria-checked="false">
Most popular
</div>
<div role="radio" aria-checked="false">
Alphabetical
</div>
</div>
<!-- No JavaScript for focus management -->
New: 1 native behavior attribute for focus routing; no roving-tabindex loop.
What focusgroup handles automatically: single tab stop entry, arrow-key navigation along the chosen inline or block axis, wrap-around from end to beginning, last-focused memory when tabbing back in, and keyboard Home / End. That's the entire roving-tabindex contract — declaratively.
see also
- Toolbar Demo — horizontal focusgroup with real UI controls
- ChromeStatus entry
- Open UI explainer