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