Toolbar

툴바

A row of buttons grouped so Tab skips the whole thing and arrow keys move between items inside it.

Also known as: Action barButton toolbar
···
html
<div class="tbw">
  <div class="tbbar" role="toolbar" aria-label="편집 도구" id="tbbar">
    <button tabindex="0" aria-label="실행 취소">↶</button>
    <button tabindex="-1" aria-label="다시 실행">↷</button>
    <span class="tbsep"></span>
    <button tabindex="-1" aria-label="저장">⇩</button>
    <button tabindex="-1" aria-pressed="false" aria-label="즐겨찾기">★</button>
    <span class="tbsep"></span>
    <div class="tbzoom" role="spinbutton" aria-label="확대/축소" aria-valuenow="100" aria-valuemin="50" aria-valuemax="200">100%</div>
  </div>
  <p class="tbnote">Tab은 툴바를 한 번에 건너뛰고, 안에서는 방향키로 이동합니다</p>
</div>
css
.tbw{position:absolute;inset:0;display:grid;place-items:center;gap:10px}
.tbbar{display:flex;align-items:center;gap:2px;background:var(--surface);border:1px solid var(--line);border-radius:9px;padding:5px}
.tbbar button{width:28px;height:28px;border:0;border-radius:6px;background:none;color:var(--fg);font-size:13px;cursor:default}
.tbbar button[aria-pressed=true]{background:var(--accent);color:#fff}
.tbbar button[data-focus]{box-shadow:0 0 0 2px var(--accent) inset}
.tbsep{width:1px;height:18px;background:var(--line);margin:0 3px}
.tbzoom{padding:0 8px;font-size:10px;color:var(--muted);font-weight:700}
.tbnote{font-size:9px;color:var(--muted);max-width:220px;text-align:center}
js
const bar = document.getElementById('tbbar');
const focusables = [...bar.querySelectorAll('button')];
const star = focusables[3];
const zoom = bar.querySelector('.tbzoom');
let focusIdx = 0, zoomVal = 100, zdir = 1, auto = true;
star.addEventListener('click', () => { auto = false; star.setAttribute('aria-pressed', String(star.getAttribute('aria-pressed') !== 'true')); });
bar.addEventListener('keydown', (e) => {
  if (e.key !== 'ArrowRight' && e.key !== 'ArrowLeft') return;
  auto = false; e.preventDefault();
  focusables[focusIdx].tabIndex = -1;
  focusIdx = (focusIdx + (e.key === 'ArrowRight' ? 1 : -1) + focusables.length) % focusables.length;
  focusables[focusIdx].tabIndex = 0; focusables[focusIdx].focus();
});
setInterval(() => {
  if (!auto) return;
  focusables.forEach((b) => b.removeAttribute('data-focus'));
  focusIdx = (focusIdx + 1) % focusables.length;
  focusables[focusIdx].setAttribute('data-focus', '');
  zoomVal += zdir * 10;
  if (zoomVal >= 150) zdir = -1;
  if (zoomVal <= 70) zdir = 1;
  zoom.textContent = zoomVal + '%';
  zoom.setAttribute('aria-valuenow', String(zoomVal));
}, 500);

A row that gathers several buttons — sometimes a small input like a spinbutton too — so Tab passes over the whole toolbar in one stop and arrow keys move between items inside it. This "roving tabindex" technique means ten or twenty buttons never cost ten or twenty Tab presses. The container is role="toolbar", only one item holds tabindex="0" at a time (the rest are -1), and that tabindex moves to the next item on every arrow-key press.

The difference from a menubar, covered there, is what happens on press — a toolbar's buttons run immediately (Save) or toggle a state (a starred favorite), and an item that opens a submenu is the rare exception mixed into a toolbar, not the rule. The rich text toolbar (see that entry) is a specific case of this pattern, with one extra requirement: its buttons must keep recomputing their pressed state to match the current selection's formatting.

Buttons aren't the only members — a small spinbutton like a zoom indicator can join the same roving-tabindex group. The core idea is that a toolbar isn't "a bunch of buttons" so much as "a group of controls sharing one Tab stop."