ARIA 접근성 가이드

컴포넌트 패턴

버튼과 토글 버튼

누르면 동작을 실행하는 버튼과, 눌림·안 눌림 두 상태를 오가는 토글 버튼을 만드는 방법입니다. 네이티브 <button>이 기본이며, aria-pressed로 토글 상태를 알립니다.

언제 쓰나요

버튼은 누르면 그 자리에서 무언가를 실행하는 컨트롤입니다. 폼 제출, 대화상자 열기, 장바구니에 담기, 목록 정렬처럼 페이지 안에서 동작을 일으킬 때 씁니다. 다른 페이지나 같은 페이지의 다른 위치로 이동한다면 버튼이 아니라 링크(<a href>)를 씁니다.

토글 버튼은 버튼 가운데 눌림(켜짐)·안 눌림(꺼짐) 두 상태를 갖는 버튼입니다. 편집기의 “굵게”, 동영상의 “음소거”, 필터 칩처럼 같은 레이블을 유지한 채 상태만 바뀌는 경우에 씁니다. 상태는 aria-pressed로 알립니다.

먼저 네이티브 요소를 쓰세요

HTML <button>은 역할(button), 키보드 포커스, Enter·Space 실행, disabled 처리, 폼 연동을 모두 기본으로 제공합니다. <div role="button">은 이 가운데 역할만 줄 뿐이므로, 나머지는 모두 스크립트로 직접 만들어야 합니다. 디자인 시스템 제약이나 레거시 코드 때문에 피할 수 없을 때만 씁니다.

이럴 때는 다른 패턴을 고르세요

이럴 때는 다른 패턴을 고르세요
상황알맞은 선택
다른 페이지·위치로 이동링크 <a href>
설정을 “켜기/끄기”로 표현 (즉시 적용되는 환경 설정)스위치 (switch) 또는 체크박스
폼에서 여러 항목 중 고르기·동의하기체크박스 <input type="checkbox">
누르면 아래 내용이 펼쳐짐디스클로저 (버튼 + aria-expanded)
누르면 명령 목록이 열림메뉴 버튼 (버튼 + aria-haspopup)
눌렀을 때 레이블 자체가 바뀜 (예: 재생 → 일시정지)일반 버튼 (레이블 변경만으로 상태 전달, aria-pressed 사용 안 함)

토글 버튼의 레이블은 바꾸지 않습니다

APG는 토글 버튼의 레이블이 상태에 따라 바뀌면 안 된다고 명시합니다. “음소거” 버튼에 aria-pressed="true"를 주면서 레이블까지 “음소거 해제”로 바꾸면 “음소거 해제, 토글 버튼, 누름”처럼 들려 지금 소리가 나는지 알기 어렵습니다. 레이블을 바꾸고 싶다면 aria-pressed를 빼고 일반 버튼으로 만듭니다.

구조

구조
종류마크업역할상태·속성
일반 버튼<button type="button">button (암묵적)필요하면 disabled 또는 aria-disabled
토글 버튼<button type="button" aria-pressed="false">button (전환 버튼으로 안내)aria-pressed = true / false (드물게 mixed)
아이콘 버튼<button aria-label="검색"><svg aria-hidden="true">buttonaria-label 또는 숨김 텍스트로 이름 제공
사용자 정의 버튼<div role="button" tabindex="0">button (명시)tabindex="0" + Enter/Space 처리 필수

버튼의 접근 가능한 이름은 기본적으로 안의 텍스트에서 계산됩니다. 화면에 보이는 텍스트가 있으면 그것이 이름이 되고, 아이콘만 있을 때만 aria-label이나 sr-only 텍스트로 이름을 붙입니다. 버튼 역할은 자식을 표현용(presentational)으로 취급하므로, 버튼 안에 제목·목록 같은 구조를 넣어도 화면낭독기에는 전달되지 않습니다.

키보드 동작

키보드 동작
키동작
Tab버튼으로 포커스를 옮깁니다. (disabled 버튼은 건너뜁니다.)
Enter버튼을 실행합니다. 토글 버튼이면 상태를 바꿉니다.
Space버튼을 실행합니다. 토글 버튼이면 상태를 바꿉니다.

실행 후 포커스 위치는 동작에 따라 다릅니다. APG 기준으로 정리하면 다음과 같습니다.

  • 대화상자를 여는 버튼: 포커스가 대화상자 안으로 이동합니다. (모달 대화상자 참고)
  • 대화상자를 닫는 버튼(“취소” 등): 대화상자를 연 요소로 포커스가 돌아갑니다.
  • 그 자리에서 끝나는 동작(토글, “적용” 등): 포커스는 대개 버튼에 그대로 남습니다.
  • 버튼이 사라지는 동작(목록 항목 “삭제” 등): 포커스가 문서 맨 처음으로 튀지 않도록 다음 항목이나 목록 제목 같은 논리적인 위치로 옮깁니다.

네이티브 버튼의 실행 시점

네이티브 <button>은 Enter를 누르는 순간(keydown), Space는 떼는 순간(keyup) click 이벤트를 발생시킵니다. div role="button"을 만들 때도 이 차이를 따라 하면 사용자가 느끼는 동작이 네이티브와 같아집니다. Space의 keydown에서는 preventDefault()로 페이지가 스크롤되지 않게 막습니다.

필요한 ARIA

button
네이티브 <button>에는 이미 들어 있으므로 따로 쓰지 않습니다. div·span을 버튼으로 만들 때만 씁니다. 이 경우 tabindex="0"과 키보드 처리를 반드시 함께 넣습니다. 역할은 “이것은 버튼이다”라는 약속일 뿐, 동작을 만들어 주지 않습니다.
aria-pressed
토글 버튼의 현재 상태입니다. true(눌림), false(안 눌림), mixed(일부만 적용, 예: 선택한 글자 일부만 굵을 때). 이 속성이 있는 것만으로 화면낭독기는 “전환 버튼”으로 안내하므로, 일반 버튼에는 넣지 않습니다. 값은 "false"로 명시해야 하며 속성을 빼면 토글 버튼이 아니게 됩니다.
aria-disabled
버튼을 비활성으로 알리되 포커스는 유지하고 싶을 때 씁니다. 네이티브 disabled는 Tab 순서에서 빠지므로 “왜 못 누르는지” 설명을 듣기 어렵습니다. aria-disabled="true"는 실제 동작을 막지 않으므로 스크립트에서 클릭·키 입력을 무시하도록 처리합니다.
aria-label · aria-labelledby
보이는 텍스트가 없는 아이콘 버튼에만 씁니다. 보이는 텍스트가 있는데 다른 aria-label을 주면 음성 명령 사용자가 화면의 글자로 버튼을 부를 수 없습니다(WCAG 2.5.3).
aria-describedby
“이 작업은 되돌릴 수 없습니다”처럼 이름 외에 보충 설명이 필요할 때 설명 요소를 연결합니다.
aria-expanded · aria-haspopup
버튼이 다른 영역을 펼치거나 팝업을 열 때 씁니다. 이 경우는 각각 디스클로저, 메뉴 버튼 패턴을 따릅니다. aria-pressed와 aria-expanded를 한 버튼에 함께 쓰지 않습니다.

직접 해 보기

버튼: div와 button 비교

마우스로 눌러 보고, Tab으로 이동해 Enter·Space로도 눌러 보세요. 왼쪽 div는 Tab으로 갈 수조차 없습니다.

잘못된 구현

장바구니에 담기

클릭 이벤트만 붙인 div입니다. 역할이 없어 화면낭독기는 일반 텍스트로 읽고, 포커스를 받을 수 없어 키보드로 누를 수 없습니다.

올바른 구현

장바구니에 담기

첫째는 네이티브 <button>, 둘째는 role="button" + tabindex="0" + 키보드 처리를 모두 갖춘 div입니다. 두 버튼은 사용자에게 똑같이 동작하지만, 둘째는 이 모든 것을 직접 구현해야 합니다.

토글 버튼: 색만 바꾸기와 aria-pressed 비교

“굵게”를 눌러 보면서 실시간 코드를 비교해 보세요.

잘못된 구현

버튼을 누르면 이 문장이 굵게 바뀝니다.

눌림 상태를 CSS 클래스(색)로만 표시합니다. 화면을 보지 못하는 사용자는 지금 켜져 있는지 알 수 없습니다.

올바른 구현

버튼을 누르면 이 문장이 굵게 바뀝니다.

“굵게”는 레이블을 고정하고 aria-pressed 값만 바꿉니다. 스타일도 [aria-pressed="true"] 선택자로 입혀 상태와 화면이 어긋나지 않습니다. “재생”은 레이블 자체가 상태를 알려 주므로 aria-pressed 없이 일반 버튼으로 둡니다.

전체 코드

HTML
<!-- 1. 기본 버튼: 가능하면 항상 네이티브 <button> -->
<button type="button" id="add">장바구니에 담기</button>

<!-- 2. 어쩔 수 없이 div를 써야 할 때: 역할 + 포커스 + 키보드를 모두 갖춥니다 -->
<div role="button" tabindex="0" id="add-custom">장바구니에 담기</div>

<!-- 3. 토글 버튼: 레이블은 고정, 상태는 aria-pressed -->
<button type="button" id="bold" aria-pressed="false">굵게</button>

<!-- 4. 레이블이 바뀌는 버튼: aria-pressed를 쓰지 않습니다 -->
<button type="button" id="play">재생</button>

<!-- 5. 아이콘만 있는 버튼: 접근 가능한 이름을 따로 줍니다 -->
<button type="button" aria-label="검색">
  <svg aria-hidden="true" focusable="false" width="16" height="16">…</svg>
</button>
JavaScript — 토글 버튼과 사용자 정의 버튼
// 1) 토글 버튼: aria-pressed 값만 바꾸고, 레이블은 그대로 둡니다.
function initToggleButton(btn, onChange) {
  btn.addEventListener('click', function () {
    var pressed = btn.getAttribute('aria-pressed') === 'true';
    btn.setAttribute('aria-pressed', String(!pressed));
    if (onChange) onChange(!pressed);
  });
}

// 2) div role="button": 네이티브 <button>이 기본으로 해 주는 일을 직접 구현합니다.
function initCustomButton(el, onActivate) {
  function isDisabled() { return el.getAttribute('aria-disabled') === 'true'; }
  el.addEventListener('click', function (e) {
    if (!isDisabled()) onActivate(e);
  });
  el.addEventListener('keydown', function (e) {
    if (isDisabled()) return;
    if (e.key === 'Enter') {          // Enter: 누르는 즉시 실행
      e.preventDefault();
      onActivate(e);
    } else if (e.key === ' ') {       // Space: 페이지 스크롤만 막고
      e.preventDefault();
    }
  });
  el.addEventListener('keyup', function (e) {
    if (e.key === ' ' && !isDisabled()) {   // 키를 뗄 때 실행 (네이티브와 같게)
      e.preventDefault();
      onActivate(e);
    }
  });
}
JavaScript — 사용 예
initCustomButton(document.getElementById('add-custom'), function () {
  // 장바구니에 담는 동작
});
initToggleButton(document.getElementById('bold'), function (pressed) {
  document.getElementById('editor').classList.toggle('is-bold', pressed);
});
비권장비권장 — 레이블과 상태를 함께 바꿈
<button type="button" aria-pressed="true">음소거 해제</button>
권장권장 — 레이블 고정, 상태만 변경
<button type="button" aria-pressed="true">음소거</button>

상태와 스타일을 한곳에서

토글 버튼의 모양은 .is-active 같은 별도 클래스 대신 button[aria-pressed="true"] 선택자로 지정하세요. ARIA 값을 바꾸지 않으면 화면도 바뀌지 않으므로, 상태를 알리지 않는 실수를 개발 중에 바로 발견할 수 있습니다. (상태 동기화 참고)

화면낭독기에서는

화면낭독기 안내 예시 (표현은 화면낭독기·브라우저·버전에 따라 조금씩 다릅니다)
상황들리는 안내 (예)
네이티브 버튼에 포커스장바구니에 담기, 버튼
div role="button" tabindex="0"에 포커스장바구니에 담기, 버튼
클릭만 붙인 div (가상 커서로 읽을 때)장바구니에 담기 (버튼이라는 안내 없음, Tab으로 도달 불가)
안 눌린 토글 버튼에 포커스굵게, 전환 버튼, 누르지 않음
토글 버튼을 눌렀을 때누름
색만 바뀌는 버튼을 눌렀을 때(아무 상태 변화도 안내되지 않음)
aria-disabled="true" 버튼에 포커스삭제, 버튼, 사용 불가

점검 체크리스트

  • 동작을 실행하는 요소는 <button>이고, 페이지 이동은 <a href>입니다.
  • 폼 안의 일반 버튼에는 type="button"을 지정해 의도치 않은 제출을 막았습니다.
  • Tab으로 모든 버튼에 도달하고, Enter와 Space 둘 다로 실행됩니다.
  • role="button"을 쓴 경우 tabindex="0", Enter/Space 처리, 포커스 표시가 모두 있습니다.
  • 모든 버튼에 접근 가능한 이름이 있고, 아이콘 버튼은 aria-label 등으로 이름을 받았습니다.
  • 보이는 텍스트가 접근 가능한 이름에 그대로 포함됩니다.
  • 토글 버튼은 aria-pressed를 "true"/"false"로 명시하고, 레이블은 상태와 상관없이 고정입니다.
  • 상태가 색으로만 구분되지 않습니다. (테두리, 체크 표시, 굵기 등 다른 단서가 함께 있습니다.)
  • 포커스 표시와 버튼 경계가 배경과 3:1 이상 대비됩니다.
  • 버튼 실행 후 포커스가 사라지거나 문서 처음으로 튀지 않습니다.

관련 WCAG 성공 기준

참고 자료