ARIA 접근성 가이드

적용 방법

키보드와 포커스 관리

키보드만으로 모든 기능을 쓸 수 있게 하는 방법입니다. 포커스 가능한 요소, tabindex 값의 의미, 로빙 tabindex와 aria-activedescendant, 포커스 이동·복귀·트랩, inert, 포커스 표시를 다룹니다.

포커스를 받는 요소

키보드 사용자는 Tab과 Shift+Tab으로 포커스를 옮기고, Enter·Space·방향키로 조작합니다. 화면낭독기 사용자, 운동 장애로 마우스를 쓰기 어려운 사용자, 스위치 장치 사용자도 결국 키보드 포커스에 의존합니다. WCAG 2.1.1 키보드는 모든 기능을 키보드로 쓸 수 있을 것을 요구합니다.

다음 HTML 요소는 기본으로 포커스를 받고 키보드 동작도 내장되어 있습니다.

  • <a href>, <area href>
  • <button>, <input>(type="hidden" 제외), <select>, <textarea> — disabled가 아닐 때
  • <summary>(<details> 안), <iframe>, controls가 있는 <audio>·<video>
  • contenteditable 요소, 스크롤 가능한 영역(일부 브라우저)

<div>나 <span>에 role="button"을 주어도 포커스와 키보드 동작은 생기지 않습니다. ARIA는 의미만 바꾸고 동작은 바꾸지 않기 때문입니다. 가능하면 네이티브 요소를 쓰고, 부득이 커스텀 위젯을 만들면 tabindex와 키 이벤트를 직접 구현해야 합니다.

비권장포커스도 키보드 동작도 없음
<div class="btn" role="button" onclick="save()">저장</div>
권장네이티브 버튼
<button type="button" onclick="save()">저장</button>

tabindex 값의 의미

tabindex 값별 동작
값Tab 순서스크립트 포커스(focus())용도
tabindex="0"들어감(문서 순서대로)가능커스텀 위젯을 Tab 순서에 넣을 때
tabindex="-1"빠짐가능스크립트로만 포커스를 보낼 대상(대화상자 제목, 오류 요약, 로빙 tabindex의 비활성 항목)
양수(tabindex="1" 이상)문서 순서보다 먼저, 숫자 순으로가능쓰지 않습니다
속성 없음요소 기본값을 따름포커스 가능한 요소만—

양수 tabindex는 쓰지 않습니다

양수 값은 페이지 전체의 Tab 순서를 문서 구조와 다르게 바꿉니다. 한 곳만 바꿔도 나머지 요소와 순서가 뒤엉키고, 유지보수 중에 쉽게 깨집니다. 순서를 바꾸고 싶으면 HTML 소스 순서를 바꾸세요. 포커스 순서가 의미와 조작 순서를 보존해야 한다는 WCAG 2.4.3 포커스 순서를 지키기 어렵게 됩니다.

HTML
<!-- 커스텀 위젯을 Tab 순서에 넣기 -->
<div role="slider" tabindex="0" aria-valuemin="0" aria-valuemax="100" aria-valuenow="40" aria-label="볼륨"></div>

<!-- Tab 순서에는 없지만 스크립트로 포커스를 보낼 제목 -->
<h1 id="page-title" tabindex="-1">주문 완료</h1>

포커스를 받지 않아야 할 정적 요소(문단, 제목, 카드 전체)에 tabindex="0"을 넣지 마세요. Tab을 누를 때마다 조작할 수 없는 요소에 멈추게 되어 키보드 사용자가 피곤해지고, 화면낭독기 사용자는 그 요소를 조작할 수 있는 것으로 오해합니다.

복합 위젯: 로빙 tabindex와 aria-activedescendant

툴바·탭 목록·라디오 그룹·리스트박스·메뉴·트리·그리드처럼 여러 항목으로 이루어진 복합 위젯은 Tab 한 번으로 위젯에 들어가고, 안에서는 방향키로 항목을 옮기는 것이 APG 키보드 인터페이스 원칙입니다. 이렇게 하면 항목이 수십 개여도 Tab 한 번으로 위젯을 지나갈 수 있습니다. 위젯 안의 “현재 항목”을 관리하는 방법은 두 가지입니다.

두 방식 비교
로빙 tabindexaria-activedescendant
실제 DOM 포커스현재 항목으로 옮겨 감컨테이너(또는 입력 필드)에 머무름
구현현재 항목만 tabindex="0", 나머지는 -1, focus() 호출컨테이너의 aria-activedescendant에 현재 항목 ID 지정
스크롤브라우저가 포커스 항목을 자동으로 보이게 함스크립트로 scrollIntoView() 처리 필요
시각 표시:focus-visible 스타일 사용현재 항목에 별도 클래스로 표시해야 함
알맞은 곳toolbar, tablist, radiogroup, menu, tree, gridcombobox(입력 필드에 포커스를 유지해야 할 때), listbox, 큰 grid

데모 1: 로빙 tabindex 툴바

Tab으로 툴바에 들어간 뒤 ←/→ 방향키, Home/End로 버튼을 옮겨 보세요. 실시간 코드에서 tabindex 값이 따라 바뀝니다. 툴바를 나갔다 다시 들어오면 마지막으로 머문 버튼에 포커스가 돌아옵니다.

현재 항목: 굵게

JavaScript
var items = toolbar.querySelectorAll('button');
function move(to) {
  items.forEach(function (b) { b.tabIndex = -1; });
  items[to].tabIndex = 0;   // 현재 항목만 Tab 순서에 남김
  items[to].focus();        // 실제 포커스 이동
}
toolbar.addEventListener('keydown', function (e) {
  var i = Array.prototype.indexOf.call(items, document.activeElement);
  if (e.key === 'ArrowRight') move((i + 1) % items.length);
  else if (e.key === 'ArrowLeft') move((i - 1 + items.length) % items.length);
  else if (e.key === 'Home') move(0);
  else if (e.key === 'End') move(items.length - 1);
  else return;
  e.preventDefault();
});

데모 2: aria-activedescendant 리스트박스

Tab으로 목록에 들어가 ↑/↓, Home/End로 옵션을 옮기고 Enter 또는 Space로 선택해 보세요. DOM 포커스는 목록(ul)에 머물고 aria-activedescendant 값만 바뀝니다.

배송 시간대

  • 오전 (9시~12시)
  • 오후 (12시~18시)
  • 저녁 (18시~21시)
  • 상관없음

선택: 오전 (9시~12시)

aria-activedescendant 사용 조건

aria-activedescendant가 가리키는 요소는 컨테이너의 하위 요소(또는 aria-owns로 소유한 요소)여야 하고, 컨테이너 자체는 실제 포커스를 받아야 합니다. 또 현재 항목에 시각적인 포커스 표시를 직접 그려야 합니다. 브라우저의 :focus 스타일은 컨테이너에만 적용되기 때문입니다. 콤보박스에서의 사용법은 콤보박스 패턴을 참고하세요.

포커스 이동과 복귀

화면 일부가 바뀔 때 포커스를 어디에 둘지 정하는 것이 키보드 접근성의 핵심입니다. 원칙은 “사용자가 다음에 할 일이 있는 곳으로 옮기고, 끝나면 원래 자리로 돌려 놓는다”입니다.

  • 대화상자를 열 때: 대화상자 안의 첫 번째 조작 요소(또는 내용이 길면 제목이나 첫 문단, tabindex="-1")로 포커스를 옮깁니다.
  • 대화상자를 닫을 때: 대화상자를 연 버튼으로 포커스를 돌려 줍니다. 그 버튼이 사라졌다면 논리적으로 이어지는 요소로 보냅니다.
  • 요소를 삭제할 때: 포커스가 있던 요소가 DOM에서 사라지면 포커스가 <body>로 떨어져 사용자가 페이지 처음으로 돌아간 것처럼 됩니다. 삭제 전에 다음 항목이나 목록 제목으로 포커스를 옮깁니다.
  • 폼 제출 오류: 오류 요약이나 첫 번째 오류 필드로 이동합니다(폼 접근성).
  • SPA 화면 전환: 새 화면의 <h1> 등으로 이동합니다(동적 콘텐츠와 SPA).
포커스 기억과 복귀
var opener = null;
function openDialog(dlg) {
  opener = document.activeElement;          // 연 요소 기억
  dlg.showModal();                          // <dialog>는 첫 조작 요소로 포커스를 자동 이동
}
function closeDialog(dlg) {
  dlg.close();
  if (opener && document.contains(opener)) opener.focus();   // 원래 자리로 복귀
}

포커스를 함부로 옮기지 마세요

사용자가 요청하지 않았는데 포커스를 옮기거나 문맥을 바꾸면 WCAG 3.2.1 초점 시, 3.2.2 입력 시에 어긋날 수 있습니다. 입력 중 자동으로 다음 필드로 넘어가는 동작, 페이지를 열자마자 아래쪽 요소로 포커스를 보내는 동작은 피합니다.

포커스 트랩과 inert

모달 대화상자가 열려 있는 동안에는 Tab 포커스가 대화상자 밖으로 나가면 안 됩니다. 뒤쪽 페이지는 시각적으로 가려졌으므로, 포커스가 그곳에 가면 사용자는 보이지 않는 요소를 조작하게 됩니다. 이렇게 포커스를 특정 영역 안에서만 돌게 하는 것을 포커스 트랩이라고 합니다.

반면, 사용자가 빠져나올 방법이 없는 트랩은 심각한 접근성 문제입니다. WCAG 2.1.2 키보드 트랩 없음은 키보드로 들어간 곳에서 키보드로 나올 수 있어야 한다고 요구합니다. 모달은 Esc와 닫기 버튼으로 반드시 닫을 수 있어야 합니다.

현재 가장 권장하는 방법은 네이티브 기능을 쓰는 것입니다.

  • <dialog> + showModal(): 대화상자 밖의 콘텐츠를 자동으로 비활성(inert) 상태로 만들고, Esc 닫기와 포커스 이동을 기본 제공합니다. dialog 역할과 aria-modal 의미도 브라우저가 처리합니다.
  • inert 속성: 요소와 그 하위 전체를 포커스·클릭·찾기(Ctrl+F)·접근성 트리에서 제외합니다. 커스텀 모달을 만들 때 대화상자 밖의 형제 요소(<header>, <main>, <footer>)에 inert를 주면 별도의 Tab 순환 스크립트 없이 트랩이 만들어집니다.
inert로 배경 비활성화 (닫을 때 inert 제거)
<body>
  <header inert>...</header>
  <main inert>...</main>
  <div role="dialog" aria-modal="true" aria-labelledby="dlg-h">
    <h2 id="dlg-h">로그아웃할까요?</h2>
    <button type="button">로그아웃</button>
    <button type="button">취소</button>
  </div>
</body>
비권장배경에 aria-hidden만 줌
<main aria-hidden="true">
  <a href="/cart">장바구니</a>  <!-- 여전히 Tab으로 포커스됨 -->
</main>
권장inert로 포커스까지 차단
<main inert>
  <a href="/cart">장바구니</a>  <!-- 포커스·클릭 불가, 트리에서도 제외 -->
</main>

각 숨김 방법의 차이는 콘텐츠 숨기기 문서에서 비교합니다. 전체 구현은 대화상자 패턴을 참고하세요.

포커스 표시

키보드 사용자는 포커스 표시(보통 외곽선)를 보고 지금 어디에 있는지 압니다. WCAG 2.4.7 초점 표시(Focus Visible, AA)는 키보드로 조작할 수 있는 모든 요소에 포커스 표시가 보일 것을 요구합니다. 가장 흔한 위반은 디자인을 이유로 outline: none을 주고 대체 스타일을 넣지 않는 것입니다.

비권장포커스 표시 제거
*:focus { outline: none; }
권장:focus-visible로 키보드 포커스 강조
:focus-visible {
  outline: 3px solid #1a5fd0;
  outline-offset: 2px;
}

:focus-visible은 브라우저가 “포커스 표시가 필요하다”고 판단할 때(주로 키보드 조작)만 적용되므로, 마우스 클릭 시 외곽선이 거슬린다는 디자인 요구와 접근성을 함께 만족시킬 수 있습니다.

WCAG 2.2에서는 관련 기준이 추가되었습니다.

포커스 표시 기본 스타일
html { scroll-padding-top: 80px; }   /* 고정 헤더 높이만큼 */
:focus-visible { outline: 3px solid #1a5fd0; outline-offset: 2px; }
@media (forced-colors: active) {
  :focus-visible { outline-color: Highlight; }   /* 고대비 모드 */
}

점검 방법

  1. 마우스를 치우고 Tab만으로 페이지 처음부터 끝까지 이동합니다. 모든 조작 요소에 도달하는지, 순서가 화면 읽기 순서와 맞는지 확인합니다.
  2. 포커스가 갈 때마다 위치가 눈에 보이는지, 고정 헤더 등에 가려지지 않는지 확인합니다.
  3. 각 위젯을 Enter, Space, 방향키, Esc로 조작해 봅니다. 위젯별 키 동작은 탭, 메뉴 버튼 등 패턴 문서를 참고합니다.
  4. 대화상자·메뉴를 열고 닫은 뒤 포커스가 원래 자리로 돌아오는지 확인합니다.
  5. 포커스가 빠져나올 수 없는 곳(키보드 트랩)이 없는지 확인합니다.