포커스 트랩

Focus Trap

모달이 열려 있는 동안 Tab이 모달 안의 요소끼리만 순환하게 가두는 기법. 뒤 배경으로 포커스가 새어나가지 않게 합니다.

···
html
<div class="stage">
  <div class="bg">
    <div class="ghost"></div><div class="ghost"></div><div class="ghost"></div>
  </div>
  <div class="modal">
    <div class="mh"></div>
    <div class="field f1"></div>
    <div class="row">
      <button class="f2"></button>
      <button class="f3"></button>
    </div>
  </div>
</div>
css
.stage{position:relative;width:94%;height:88%}
.bg{position:absolute;inset:0;display:flex;gap:10%;padding:10%;filter:blur(1.5px);opacity:.35}
.ghost{flex:1;border-radius:10px;background:var(--muted)}
.modal{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:70%;height:76%;border-radius:14px;border:1px solid var(--line);background:var(--surface);box-shadow:0 12px 30px rgba(0,0,0,.18);padding:9%;display:flex;flex-direction:column;gap:10%}
.mh{width:50%;height:12%;border-radius:5px;background:var(--fg);opacity:.7}
.field{height:20%;border-radius:8px;border:1px solid var(--line);background:var(--bg)}
.row{display:flex;gap:8%;flex:1}
.row button{flex:1;border-radius:8px;border:1px solid var(--line);background:var(--bg)}
.ring{box-shadow:0 0 0 3px var(--accent)}
js
const order = ['.f1', '.f2', '.f3'];
let i = 0;
function tick() {
  order.forEach(function (sel) { document.querySelector(sel).classList.remove('ring'); });
  document.querySelector(order[i % order.length]).classList.add('ring');
  i++;
}
tick();
setInterval(tick, 900);

모달을 열어놓고 마지막 요소에서 Tab을 계속 누르면, 아무 조치가 없을 경우 포커스가 모달 뒤에 가려진 배경 콘텐츠로 넘어가 버립니다. 화면으로는 안 보이는데 스크린 리더는 그 요소를 읽어버리는, 매우 혼란스러운 상태입니다. 포커스 트랩은 모달 안의 첫 요소와 마지막 요소를 연결해 Tab이 그 안에서만 순환하게 만듭니다.

이름은 "트랩"이지만 사용자를 가두려는 게 아니라 **탈출구를 하나로 명확히 만드는** 것이 목적입니다 — Esc 키나 닫기 버튼으로 언제든 나갈 수 있어야 합니다. 탈출구 없이 진짜로 가두면 오히려 2.1.2(No Keyboard Trap) 위반이 됩니다.

데모는 모달 안 3개 요소를 Tab이 자동으로 순환하는 과정을 보여줍니다 — 마지막에서 Tab을 누르면 다시 첫 요소로, 배경(흐려진 부분)으로는 절대 넘어가지 않습니다.

언제 쓰나

직접 구현하기보다는 검증된 라이브러리(focus-trap, Radix)나 브라우저 네이티브 <dialog>의 showModal()을 쓰세요. <dialog>는 브라우저가 트랩과 Esc 닫기를 기본 제공합니다.