툴바

Toolbar

버튼 여러 개를 한 줄에 모아, Tab은 통째로 건너뛰고 안에서는 방향키로만 이동하게 만드는 컨테이너.

다른 이름: 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);

버튼(때로는 작은 스핀버튼 같은 입력 하나까지) 여러 개를 한 줄에 모아, Tab 키로는 툴바 전체를 한 번에 지나치고 그 안에서는 방향키로만 이동하게 만드는 컨테이너입니다 — "로빙 탭인덱스"라 부르는 이 방식 덕분에 버튼이 10개든 20개든 Tab을 그만큼 눌러야 하는 일이 없습니다. 컨테이너는 role="toolbar", 한 번에 하나의 항목만 tabindex="0"(나머지는 -1)을 가지며 방향키를 누를 때마다 그 tabindex가 다음 항목으로 옮겨갑니다.

메뉴바와 다른 점은 위에서 다룬 대로 눌렀을 때의 동작입니다 — 툴바의 버튼은 눌리는 즉시 실행되거나(저장) 상태를 토글하고(즐겨찾기 별표), 하위 메뉴를 펼치는 항목은 툴바 안에 섞여 들어가는 예외적인 한 버튼 정도입니다. 서식 툴바(이 사전의 다른 항목)는 이 패턴의 특수한 사례로, 버튼들이 지금 선택 영역의 서식 상태에 맞춰 계속 눌림 상태를 재계산해야 한다는 조건이 하나 더 붙습니다.

버튼뿐 아니라 확대/축소 표시 같은 작은 스핀버튼도 같은 로빙 탭인덱스 그룹에 넣을 수 있습니다 — 툴바는 "버튼 모음"이 아니라 "한 번의 Tab 정지로 묶인 컨트롤 모음"이라는 점이 핵심입니다.