키보드 네비게이션
19.3.1. 포커스 관리
모든 인터랙티브 UI 요소는 키보드로 접근하고 조작할 수 있어야 합니다. tabindex 속성은 다음 규칙에 따라 사용합니다.
| tabindex 값 | 의미 | 사용 규칙 |
|---|---|---|
0 | 자연스러운 Tab 순서에 포함 | 비인터랙티브 요소를 포커스 가능하게 만들 때 사용 |
-1 | Tab 순서에서 제외, 프로그래밍 방식으로 포커스 가능 | 모달, 동적 콘텐츠 등에서 스크립트로 포커스를 이동할 때 사용 |
양수 (1 이상) | 사용 금지 | Tab 순서를 임의로 변경하여 예측 불가능한 동작을 유발함 |
<button>,<a>,<input>등 네이티브 인터랙티브 요소는 기본적으로 포커스를 받으므로tabindex를 별도로 지정하지 않습니다.<div>,<span>등 비인터랙티브 요소에 클릭 이벤트를 바인딩하는 것을 지양합니다. 대신<button>요소를 사용합니다.
포커스 표시기(Focus Ring)는 모든 포커스 가능 요소에 시각적으로 표시해야 합니다.
css
/* 포커스 표시기 기본 스타일 */
:focus-visible {
outline: 2px solid #2563eb;
outline-offset: 2px;
}
/* outline을 제거하는 것은 금지 */
/* 잘못된 예: *:focus { outline: none; } */outline: none으로 포커스 표시기를 제거하지 않습니다. 커스텀 스타일로 대체하는 경우에만outline: none을 사용하되, 반드시 동등한 시각적 표시를 제공해야 합니다.:focus-visible을 사용하여 키보드 포커스에만 표시기를 적용하는 것을 권장합니다.
19.3.2. Tab 순서
Tab 키로 이동하는 순서는 콘텐츠의 논리적 흐름과 시각적 배치 순서를 따라야 합니다.
- DOM 순서가 시각적 순서와 일치해야 합니다. CSS의
order,flex-direction: row-reverse,position: absolute등으로 시각적 순서만 변경하면 Tab 순서와 불일치가 발생합니다. - 레이아웃을 CSS로 재배치해야 하는 경우, DOM 순서 자체를 변경하여 논리적 흐름을 유지합니다.
- 동적으로 삽입된 콘텐츠(토스트, 드롭다운 등)가 Tab 순서를 방해하지 않도록 배치합니다.
vue
<!-- 올바른 예: DOM 순서 = 시각적 순서 -->
<template>
<nav>
<a href="/home">홈</a>
<a href="/about">소개</a>
<a href="/contact">문의</a>
</nav>
<main>
<h1>페이지 제목</h1>
<p>본문 내용</p>
</main>
</template>
<!-- 잘못된 예: CSS로 시각적 순서만 역전 -->
<!--
<template>
<div class="flex flex-row-reverse">
<button>첫 번째로 보이지만 Tab은 마지막</button>
<button>마지막으로 보이지만 Tab은 첫 번째</button>
</div>
</template>
-->19.3.3. 키보드 단축키
인터랙티브 컴포넌트는 다음 키보드 동작을 지원해야 합니다.
| 키 | 동작 | 적용 대상 |
|---|---|---|
Enter | 버튼 클릭, 폼 제출, 링크 활성화 | 버튼, 폼, 링크 |
Escape | 모달 닫기, 드롭다운 닫기, 작업 취소 | 모달, 드롭다운, 팝오버 |
Space | 체크박스 토글, 버튼 클릭 | 체크박스, 버튼 |
| 방향키 | 탭 패널 전환, 메뉴 항목 이동, 라디오 버튼 선택 | 탭, 메뉴, 라디오 그룹 |
vue
<script setup lang="ts">
import { ref } from 'vue'
const isOpen = ref(false)
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') {
isOpen.value = false
}
}
</script>
<template>
<div @keydown="handleKeydown">
<button @click="isOpen = !isOpen">
메뉴 열기
</button>
<ul v-if="isOpen" role="menu">
<li role="menuitem" tabindex="0">항목 1</li>
<li role="menuitem" tabindex="0">항목 2</li>
<li role="menuitem" tabindex="0">항목 3</li>
</ul>
</div>
</template>@keydown이벤트로 키보드 동작을 처리합니다.@keypress는 사용하지 않습니다.- Vue의 키 수식어(
@keydown.escape,@keydown.enter)를 활용하는 것을 권장합니다. - 커스텀 키보드 단축키를 제공하는 경우, 브라우저 및 운영 체제의 기본 단축키와 충돌하지 않아야 합니다.
19.3.4. 스킵 네비게이션
반복되는 내비게이션 영역을 건너뛰고 본문 콘텐츠로 직접 이동할 수 있는 스킵 네비게이션 링크를 제공해야 합니다.
vue
<template>
<a
href="#main-content"
class="skip-nav"
>
본문 바로가기
</a>
<header>
<nav>
<!-- 내비게이션 항목 -->
</nav>
</header>
<main id="main-content" tabindex="-1">
<!-- 본문 콘텐츠 -->
</main>
</template>
<style scoped>
.skip-nav {
position: absolute;
top: -100%;
left: 0;
z-index: 9999;
padding: 8px 16px;
background-color: #ffffff;
color: #1a1a1a;
font-weight: bold;
text-decoration: none;
}
.skip-nav:focus {
top: 0;
}
</style>- 스킵 네비게이션 링크는 페이지의 최상단 첫 번째 요소로 배치해야 합니다.
- 평상시에는 화면에 보이지 않으며, 포커스를 받았을 때만 화면에 표시됩니다.
- 링크 대상(
#main-content)에tabindex="-1"을 지정하여 포커스를 수신할 수 있도록 합니다. - 내비게이션 영역이 복잡한 경우, 검색 영역이나 사이드바로의 바로가기 링크를 추가로 제공할 수 있습니다.