Roving Tabindex

로빙 탭인덱스

The pattern where Tab visits a composite widget — a toolbar, a listbox — just once, and arrow keys handle movement inside it.

Also known as: roving tabindex pattern
···
html
<div class="stage">
  <div class="toolbar">
    <div class="item i0"><div class="ic"></div><div class="ti">0</div></div>
    <div class="item i1"><div class="ic"></div><div class="ti">-1</div></div>
    <div class="item i2"><div class="ic"></div><div class="ti">-1</div></div>
    <div class="item i3"><div class="ic"></div><div class="ti">-1</div></div>
  </div>
  <div class="hint" id="hint">⇥ Tab enters once</div>
</div>
css
.stage{width:92%;height:80%;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12%}
.toolbar{display:flex;gap:6%;padding:5% 6%;border-radius:12px;border:1px solid var(--line);background:var(--surface)}
.item{display:flex;flex-direction:column;align-items:center;gap:8px}
.ic{width:11vmin;max-width:44px;aspect-ratio:1;border-radius:9px;border:1px solid var(--line);background:var(--bg)}
.item.active .ic{box-shadow:0 0 0 3px var(--accent)}
.ti{font:700 11px/1 ui-monospace,monospace;color:var(--muted)}
.item.active .ti{color:var(--accent);font-weight:800}
.hint{font:700 12px/1 ui-monospace,monospace;color:var(--muted);min-height:14px}
js
const items = [0, 1, 2, 3].map(function (i) { return document.querySelector('.i' + i); });
const hint = document.getElementById('hint');
let active = 0;
function render() {
  items.forEach(function (el, i) {
    el.classList.toggle('active', i === active);
    el.querySelector('.ti').textContent = i === active ? '0' : '-1';
  });
}
render();
let step = 0;
function tick() {
  if (step === 0) {
    hint.textContent = '⇥ Tab enters once';
  } else {
    active = (active + 1) % items.length;
    render();
    hint.textContent = '→ moves the roving index';
  }
  step = (step + 1) % items.length;
}
setInterval(tick, 1000);

If a menu bar's 8 items were each their own Tab stop, a user would need 8 presses of Tab just to get past that one menu. The roving tabindex pattern keeps **exactly one** item at `tabindex="0"` inside the widget and every other item at `tabindex="-1"`, so Tab treats the whole widget as a single stop.

Once focus lands inside the widget, arrow keys take over movement by shifting which item holds `tabindex="0"` — the old item drops to -1, the new one becomes 0 and actually receives focus. This is the core mechanism behind ARIA composite widget patterns: toolbars, tabs, listboxes, menus.

The demo shows the tabindex value on each of a 4-item toolbar flipping between 0 and -1 as arrow-key presses move the active item.

When to use

Don't make every item in a hand-built toolbar, tab list, or listbox its own Tab stop. Past 2-3 items, switch to roving tabindex or aria-activedescendant.