v150 · 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
// 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');
});
});
~40 lines of JS (per widget)
✅ After — focusgroup
1 HTML attr
Newest first
Oldest first
Most popular
Alphabetical
<!-- The entire keyboard navigation is declared here -->
<div role="radiogroup"
focusgroup="radiogroup block nomemory">
<div role="radio" tabindex="0" aria-checked="true" focusgroupstart>
Newest first
</div>
<div role="radio" tabindex="0" aria-checked="false">
Oldest first
</div>
<div role="radio" tabindex="0" aria-checked="false">
Most popular
</div>
<div role="radio" tabindex="0" aria-checked="false">
Alphabetical
</div>
</div>
<!-- No JavaScript for focus management -->
0 JS lines for focus management
What focusgroup handles automatically: single tab stop entry, behavior-token arrow-key navigation, wrap defaults for patterns such as radiogroup and menubar, and last-focused memory when enabled. Selection state still belongs to the widget's JavaScript.
see also
- Toolbar Demo — toolbar focusgroup with real UI controls
- ChromeStatus entry
- Open UI explainer
implementation reference
Need the exact API surface, compatibility boundaries, errors, lifecycle, and source links? Read the matching gendn reference ↗