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
// 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
<!-- 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

implementation reference

Need the exact API surface, compatibility boundaries, errors, lifecycle, and source links? Read the matching gendn reference ↗