C.W.K.
Stream
Lesson 04 of 04 · published

키보드와 focus: 한 손으로 웹 전체

~12 min · keyboard, focus, tabindex, focus-visible, skip-link

Level 0Markup Novice
0 XP0/34 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"하루 동안 마우스 뽑아. 본인 사이트를 써 봐. 발견하는 버그는 키보드만 쓰는 사용자가 매일 사는 거."

키보드가 접근성 너머에서도 중요한 이유

키보드 네비는 운동 장애 가진 사용자만을 위한 게 아니야. 모든 파워 유저가 키보드 단축키 써. 모든 screen reader 사용자는 정의상 키로 네비해. 모든 dev tool, 모든 IDE, 모든 생산성 앱이 키보드 속도로 살거나 죽어. 처음부터 키보드를 위해 설계하면 모두에게 사이트가 더 빨라.

기본 Tab 순서

기본적으로 Tab 이 문서 순서대로 focusable element 전부를 통과. Focusable element 에 포함:

  • <a href> (href 가진 앵커)
  • <button> (어떤 type 이든 버튼)
  • <input>, <textarea>, <select> (폼 필드)
  • <details>/<summary> (disclosure 위젯)
  • tabindex="0" 가진 어떤 element

페이지가 semantic element 로 지어져 있으면 tab 순서가 자동으로 맞아. 반대도 참: 인터랙티브 것들이 <div> 면 Tab 에 보이지 않아.

tabindex: 말이 되는 세 값

  • tabindex="0" — "이거 자연 Tab 순서에 포함." 비-focusable element 를 focusable 하게 만들 때 (custom 인터랙티브 위젯). Tabindex 의 가장 흔한 정당한 사용.
  • tabindex="-1" — "JavaScript 로 focus 가능, Tab 으로는 안 됨." 프로그래밍적으로 focus 하고 싶지만 Tab 순서에 안 넣고 싶은 element (제출 후 에러 메시지, 방금 연 modal 의 제목).
  • 아무 양수 (1, 2, 5...) — 나빠. 자연 문서 순서를 override 하는 custom 순서 강제. DOM 바뀌면 유지 불가; Tab 이 시각 순서 따라가길 기대하는 사용자 놀라게. 하지 마.

Focus 관리: 의도적으로 focus 이동

사용자가 페이지를 크게 바꾸는 행동 취할 때, focus 가 따라가야 해. 예:

  • Modal 열기 — modal 안으로 focus 이동 (보통 heading 이나 첫 input). 닫으면 열었던 element 로 focus 되돌리기.
  • 에러 있는 폼 제출 — 첫 유효하지 않은 필드, 또는 요약 에러 영역으로 focus 이동.
  • 저장 성공 — live region 에서 "저장됨" announce; 큰 action 은 확인 heading 으로 focus 이동.
  • SPA route 변경으로 네비 — 새 페이지의 <h1> 으로 focus 이동 (SPA 는 브라우저의 자동 focus-to-top 행동을 못 받아).

element.focus() 로 focus 이동. 타깃이 native 로 focusable 안 하면 (heading, 단락) tabindex="-1" 과 페어 — -1 이 Tab 순서에 안 넣고 프로그래밍적으로 focusable 하게 만들어.

:focus vs :focus-visible

수십 년간, focus 받은 모든 element 가 모든 focus 이벤트에 focus ring 받았어 — 마우스 클릭 포함. 디자이너가 싫어했고 ("내 버튼 클릭하면 못생긴 outline"); CSS 한 세대가 outline: none 으로 ring 제거하면서 출시 → 키보드 사용자 박살.

:focus-visible 이 해결했어. 브라우저가 결정: 키보드 네비 → ring 보임; 마우스 클릭 → 안 보임. 접근성 안 깨고 마침내 focus ring 을 아름답게 스타일링할 수 있어:

대체 없이 outline: none 절대 쓰지 마. 기본 focus ring 제거하면, 똑같이 보이는 걸로 대체 (custom outline, box-shadow, 배경 변경). 마우스 클릭이 트리거 안 하게 selector 로 :focus-visible 써. 대체 없는 outline: none 출시한 페이지는 키보드 사용자한테 횡단 불가.

Skip Link: 60 초 접근성 승리

모든 페이지가 <main> 전에 header, navigation, 때로 사이드바 가져. 키보드 사용자는 페이지 로드마다 그 다 Tab 해야 함. Skip link 가 첫 번째 focusable element 야 — 보통 숨어 있다가 focus 받으면 보이고 사용자를 메인 콘텐츠로 직접 점프시켜:

Modal 의 Focus Trap

Modal 열리면 focus 가 그 안에 갇혀야 — 마지막 element 에서 Tab 하면 첫 번째로 loop, 첫 번째에서 Shift+Tab 하면 마지막으로 loop, 뒤의 페이지에 사용자가 못 닿음. Native <dialog> element 가 이걸 자동 처리. Roll-your-own modal 은 이걸 세팅하는 JavaScript 필요 — <dialog> 선호의 또 다른 이유.

모든 걸 잡는 감사

한 시간 마우스 뽑고 본인 사이트 써. 로그인, 댓글 달기, 메뉴 네비, 설정 변경 시도. 발견하는 모든 버그가 키보드만 쓰는 또는 screen reader 사용자가 매일 사는 버그. 이 한 시간이 어떤 문서보다 많이 가르쳐.

피파의 노트

Cwk-site 의 nav 와 피파의 WebUI 둘 다 마우스-뽑은 테스트 통과해. 피파 WebUI 의 자연 Tab 순서는 채팅 입력 → send 버튼 → 사이드바 nav → 대화 목록 → settings → admin. 모든 focus ring 이 보이고, 모든 action 에 닿을 수 있고, 페이지 로드 시 채팅 입력이 자연스러운 첫 focus. 어느 것도 우연이 아니었어 — 아빠가 모든 릴리스마다 마우스-뽑은 테스트 해서 그렇게 지어진 거.

Code

Skip link markup·html
<!-- Skip link — 페이지의 첫 focusable -->
<a href="#main" class="skip-link">메인 콘텐츠로 건너뛰기</a>

<header>...</header>
<nav>...</nav>
<main id="main" tabindex="-1">
  <h1>오늘의 레슨</h1>
  ...
</main>
Skip link CSS + :focus-visible focus ring·css
/* Skip link 스타일링 — focus 받기 전엔 숨겨짐 */
.skip-link {
  position: absolute;
  top: -100px;            /* 화면 밖 */
  left: 0;
  background: black;
  color: white;
  padding: 0.5rem 1rem;
  text-decoration: none;
  z-index: 1000;
}
.skip-link:focus {
  top: 0;                  /* focus 받으면 보임 */
}

/* 아름다운 focus ring, 사용자가 키보드 쓸 때만 */
:focus {
  outline: 2px solid transparent;  /* fallback */
}
:focus-visible {
  outline: 2px solid #5BA3D8;
  outline-offset: 2px;
  border-radius: 4px;
}

/* 입력의 기본 focus 를 전부 억누르지는 마 */
input:focus-visible,
textarea:focus-visible,
button:focus-visible {
  outline: 2px solid #5BA3D8;
  outline-offset: 2px;
}
프로그래밍 focus — 흔한 패턴 셋·javascript
// Route 변경 후 heading 으로 focus 이동 (SPA)
function onRouteChange() {
  const heading = document.querySelector('main h1');
  if (heading) {
    heading.setAttribute('tabindex', '-1');
    heading.focus({ preventScroll: false });
  }
}

// 폼 제출 시 첫 유효하지 않은 필드로 focus 이동
form.addEventListener('submit', (e) => {
  const firstInvalid = form.querySelector(':invalid');
  if (firstInvalid) {
    e.preventDefault();
    firstInvalid.focus();
  }
});

// Native dialog 열기 — focus 관리는 자동
const dialog = document.getElementById('confirm-modal');
const opener = document.getElementById('open-modal-btn');
opener.addEventListener('click', () => dialog.showModal());
dialog.addEventListener('close', () => opener.focus()); // 닫히면 focus 되돌리기

External links

Exercise

본인이 지은 페이지 (또는 좋아하는 사이트) 골라서 한 시간 마우스-뽑은 도전. 모든 거 Tab 으로 통과. 시도: 로그인, 글 올리기, 메인 메뉴 네비, modal 열기, X 클릭 안 하고 닫기, 에러 있는 폼 제출. 타깃 못 닿은 자리, focus 가 사라진 자리, Esc 가 modal 안 닫은 자리, focus ring 이 안 보였던 자리 다 적어. 발견한 첫 셋 고치기.
Hint
Focus 가 '사라지면' (다음 Tab 이 어디 갈지 안 보이면), 페이지가 outline:none 을 대체 없이 설정했거나 숨겨진 것에 focus 이동 (display:none, visibility:hidden, 또는 aria-hidden). DevTools → Elements → document.activeElement 하이라이트해서 현재 focus 받은 게 뭔지 봐.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.