ARIA 접근성 가이드

컴포넌트 패턴

아코디언

제목 목록처럼 늘어선 여러 구역을 각각 펼치고 접는 패턴입니다. 각 제목 안의 버튼이 aria-expanded로 상태를 알리고, 패널은 필요하면 role="region"으로 제목과 연결합니다.

언제 쓰나요

아코디언은 제목이 붙은 여러 구역을 세로로 쌓고, 각 제목을 눌러 해당 내용을 펼치고 접게 하는 패턴입니다. 자주 묻는 질문(FAQ), 긴 설정 화면, 상품 상세 정보처럼 사용자가 필요한 구역만 골라 읽는 경우에 적합합니다. 구조적으로는 디스클로저 여러 개를 일관된 제목 구조로 묶은 것입니다.

  • 네이티브 대안: <details>/<summary>를 여러 개 두면 스크립트 없이 아코디언을 만들 수 있습니다. 같은 name 속성을 주면 하나만 열리는 배타적 아코디언이 됩니다(최신 브라우저 지원).
  • 한 번에 하나의 구역만 보이고 구역 간 전환이 핵심이라면 탭이 더 알맞을 수 있습니다. 반대로 화면이 좁거나 여러 구역을 함께 열어 비교해야 하면 아코디언이 낫습니다.
  • 내용이 짧고 모두 중요하다면 접지 말고 그냥 보여 주는 편이 좋습니다. 접힌 내용은 페이지 검색이나 화면낭독기 가상 커서로 찾을 수 없습니다.

구조

아코디언 구조
div.accordion
├─ h3                         ← 제목 (제목 탐색으로 이동 가능)
│  └─ button  aria-expanded="true"  aria-controls="panel-1"  id="btn-1"
├─ div#panel-1  role="region"  aria-labelledby="btn-1"      ← 펼쳐진 패널
├─ h3
│  └─ button  aria-expanded="false" aria-controls="panel-2"  id="btn-2"
└─ div#panel-2  role="region"  aria-labelledby="btn-2"  hidden
구조
요소역할속성
아코디언 제목heading (<h2>~<h6>)페이지 제목 구조에 맞는 수준. div role="heading"이면 aria-level 필요
제목 버튼buttonaria-expanded, aria-controls, 접을 수 없는 열린 패널이면 aria-disabled
패널(선택) regionaria-labelledby → 제목 버튼 id

제목 요소 안에는 버튼 하나만 둡니다. 제목 요소 자체에 role="button"을 주면 제목 의미가 덮어써지므로, 반드시 제목이 버튼을 감싸는 구조로 만듭니다.

키보드 동작

키보드 동작
키동작
Enter 또는 Space포커스가 있는 제목의 패널을 펼치거나 접습니다. 한 번에 하나만 펼치는 아코디언이면 다른 패널을 접습니다.
Tab다음 포커스 가능한 요소로 이동합니다. 제목 버튼과 펼쳐진 패널 안의 링크·입력 요소가 모두 Tab 순서에 들어 있습니다.
Shift + Tab이전 포커스 가능한 요소로 이동합니다.
↓ (선택)다음 아코디언 제목으로 포커스를 옮깁니다. 마지막에서는 첫 제목으로 돌아가거나 멈춥니다.
↑ (선택)이전 아코디언 제목으로 포커스를 옮깁니다.
Home (선택)첫 아코디언 제목으로 이동합니다.
End (선택)마지막 아코디언 제목으로 이동합니다.

화살표 키는 APG에서 선택 사항입니다. 넣더라도 Tab 이동을 대체하지 않으며, 제목 버튼들은 모두 Tab 순서에 남아 있어야 합니다(탭 패턴과 다른 점).

필요한 ARIA

heading · aria-level
각 제목은 문서 구조에 맞는 제목 요소입니다. 화면낭독기 사용자는 제목 단위로 건너뛰며 원하는 구역을 찾습니다. <h3> 같은 네이티브 요소를 쓰면 aria-level은 필요 없습니다.
button · aria-expanded
제목 안의 버튼이 펼침 상태를 갖습니다. 펼쳐지면 true, 접히면 false입니다. 버튼의 이름은 제목 텍스트가 됩니다.
aria-controls
버튼이 제어하는 패널의 id를 가리킵니다. 스크립트에서 패널을 찾는 기준으로도 씁니다.
region · aria-labelledby (선택)
패널을 이름 있는 랜드마크 구역으로 만들어, 패널 안에 있다가도 어느 구역인지 알 수 있게 합니다. APG는 패널이 많을 때(대략 6개 넘게 동시에 펼쳐질 수 있을 때) 랜드마크가 지나치게 늘어나므로 region을 쓰지 말라고 권합니다. 이때는 역할 없이 div로 둡니다.
aria-disabled
“항상 하나는 열려 있어야 하는” 아코디언에서 이미 열린 패널의 버튼을 눌러도 접히지 않는다면, 그 버튼에 aria-disabled="true"를 주어 지금은 접을 수 없다는 사실을 알립니다.

직접 해 보기

Tab으로 이동해 Enter/Space로 펼치고, 올바른 구현에서는 ↑ ↓ Home End로 제목 사이를 이동해 보세요. “한 번에 하나만 펼치기”를 켜면 동작 방식이 바뀝니다.

잘못된 구현

배송은 얼마나 걸리나요?
교환·반품은 어떻게 하나요?
영수증을 다시 받을 수 있나요?

질문을 클릭 가능한 div로 만들었습니다. 키보드로 펼칠 수 없고, 제목 구조와 펼침 상태가 모두 빠져 있어 화면낭독기 사용자는 이것이 접힌 질문 목록이라는 사실조차 알 수 없습니다.

올바른 구현

첫 번째 버튼과 패널이 실시간 코드에 표시됩니다. 펼칠 때마다 aria-expanded와 hidden이 함께 바뀝니다.

전체 코드

HTML
<div class="accordion">
  <h3>
    <button type="button" class="ac-trigger" id="ac-btn-1"
            aria-expanded="false" aria-controls="ac-panel-1">
      <span class="ac-icon" aria-hidden="true"></span>배송은 얼마나 걸리나요?
    </button>
  </h3>
  <div id="ac-panel-1" role="region" aria-labelledby="ac-btn-1" hidden>
    <p>평일 오후 2시 이전 주문은 당일 출고하며, 보통 1~2일 안에 도착합니다.</p>
  </div>

  <h3>
    <button type="button" class="ac-trigger" id="ac-btn-2"
            aria-expanded="false" aria-controls="ac-panel-2">
      <span class="ac-icon" aria-hidden="true"></span>교환·반품은 어떻게 하나요?
    </button>
  </h3>
  <div id="ac-panel-2" role="region" aria-labelledby="ac-btn-2" hidden>
    <p>상품을 받은 날부터 7일 안에 마이페이지에서 신청할 수 있습니다.</p>
  </div>
</div>

<script>
  initAccordion(document.querySelector('.accordion'), { single: false });
</script>
JavaScript
// 아코디언: 제목(h3) 안의 버튼 + 패널 묶음
// options.single = true 이면 하나를 펼칠 때 나머지를 접습니다.
function initAccordion(root, options) {
  options = options || {};
  var triggers = Array.prototype.slice.call(root.querySelectorAll('.ac-trigger'));

  function panelOf(btn) { return document.getElementById(btn.getAttribute('aria-controls')); }
  function setOpen(btn, open) {
    btn.setAttribute('aria-expanded', String(open));
    panelOf(btn).hidden = !open;
  }

  triggers.forEach(function (btn) {
    btn.addEventListener('click', function () {
      var open = btn.getAttribute('aria-expanded') !== 'true';
      if (open && options.single) {
        triggers.forEach(function (other) { if (other !== btn) setOpen(other, false); });
      }
      setOpen(btn, open);
    });

    // (선택) 제목 사이 화살표 이동
    btn.addEventListener('keydown', function (e) {
      var i = triggers.indexOf(btn), next = null;
      if (e.key === 'ArrowDown') next = triggers[(i + 1) % triggers.length];
      else if (e.key === 'ArrowUp') next = triggers[(i - 1 + triggers.length) % triggers.length];
      else if (e.key === 'Home') next = triggers[0];
      else if (e.key === 'End') next = triggers[triggers.length - 1];
      if (next) { e.preventDefault(); next.focus(); }
    });
  });
}
HTML — 네이티브 대안 (details name)
<!-- 같은 name을 가진 details는 하나만 열립니다 (배타적 아코디언) -->
<details name="faq">
  <summary>배송은 얼마나 걸리나요?</summary>
  <p>평일 오후 2시 이전 주문은 당일 출고합니다.</p>
</details>
<details name="faq">
  <summary>교환·반품은 어떻게 하나요?</summary>
  <p>상품을 받은 날부터 7일 안에 신청할 수 있습니다.</p>
</details>

“모두 펼치기” 버튼

구역이 많으면 아코디언 위에 “모두 펼치기/모두 접기” 버튼을 두면 편합니다. 이 버튼은 일반 버튼이며, 동작 후 각 제목 버튼의 aria-expanded도 모두 함께 갱신해야 합니다.

비권장비권장 — 제목 자체를 버튼으로 (제목 의미가 사라짐)
<h3 role="button" tabindex="0" aria-expanded="false">배송은 얼마나 걸리나요?</h3>
권장권장 — 제목이 버튼을 감쌈
<h3><button type="button" aria-expanded="false" aria-controls="p1">배송은 얼마나 걸리나요?</button></h3>

화면낭독기에서는

화면낭독기 안내 예시 (표현은 화면낭독기·브라우저·버전에 따라 조금씩 다릅니다)
상황들리는 안내 (예)
제목 탐색(H 또는 3)으로 첫 질문 도착제목 수준 3, 배송은 얼마나 걸리나요?, 버튼, 축소됨
Tab으로 제목 버튼에 포커스배송은 얼마나 걸리나요?, 버튼, 축소됨
Enter로 펼침확장됨
펼친 패널 안으로 읽어 들어감 (region 사용 시)배송은 얼마나 걸리나요?, 영역, 평일 오후 2시 이전 주문은 …
한 번에 하나만 열리는데 열린 패널을 접을 수 없는 경우배송은 얼마나 걸리나요?, 버튼, 확장됨, 사용 불가

점검 체크리스트

  • 각 아코디언 제목은 페이지 구조에 맞는 제목 요소이고, 그 안에 <button>이 하나 있습니다.
  • 제목 버튼에 aria-expanded가 있고, 화면 상태와 항상 일치합니다.
  • aria-controls가 실제 패널 id를 가리킵니다.
  • 패널에 role="region"을 썼다면 aria-labelledby로 제목 버튼과 연결했고, 패널 수가 지나치게 많지 않습니다.
  • 접힌 패널은 hidden으로 숨겨져 안의 요소가 Tab으로 잡히지 않습니다.
  • 모든 제목 버튼이 Tab 순서에 있고, 화살표 키를 넣었다면 Tab 이동도 그대로 동작합니다.
  • 패널이 DOM 순서상 해당 제목 바로 뒤에 있습니다.
  • 접을 수 없는 열린 패널의 버튼에는 aria-disabled="true"가 있습니다.
  • 펼침 아이콘은 장식으로 처리되어 버튼 이름에 섞이지 않습니다.

관련 WCAG 성공 기준

참고 자료