컴포넌트 패턴
디스클로저(펼치기/접기)
버튼 하나로 관련 내용을 펼치고 접는 패턴입니다. 버튼의 aria-expanded로 상태를 알리고, 접힌 내용은 hidden으로 숨깁니다. 가장 간단한 대안은 네이티브 <details>입니다.
언제 쓰나요
디스클로저(disclosure)는 버튼 하나가 내용 하나를 펼치고 접는 가장 단순한 표시/숨김 패턴입니다. 자주 묻는 질문의 답, “자세히 보기”로 감춘 부가 설명, 고급 검색 옵션, 사이트 내비게이션의 하위 메뉴 목록처럼 처음에는 감춰 두어도 되는 내용에 씁니다.
- 먼저 네이티브
<details>·<summary>를 검토하세요. 펼침 상태 안내, 키보드 조작, 스크립트 없이도 동작하는 점까지 브라우저가 제공합니다. 다만<summary>안에 제목 요소를 넣으면 제목 탐색에서의 표현이 브라우저마다 다르고, 스타일 제약이 있습니다. - 디스클로저 여러 개를 묶어 제목 목록처럼 보여 주면 아코디언입니다.
- 열린 내용이 다른 내용 위에 겹쳐 뜨고 바깥을 누르면 닫혀야 한다면 팝업 계열(메뉴 버튼, 대화상자, 콤보박스)을 검토합니다.
- 사이트의 상단 내비게이션 드롭다운은 대부분
role="menu"가 아니라 디스클로저로 만드는 것이 맞습니다. 펼쳐지는 것은 명령 목록이 아니라 링크 목록이기 때문입니다.
꼭 필요한 정보는 숨기지 마세요
가격, 오류 안내, 필수 입력 조건처럼 모든 사용자가 반드시 봐야 하는 내용은 접어 두지 않습니다. 접힌 내용은 화면낭독기의 가상 커서로도 읽히지 않으므로, 존재 자체를 놓칠 수 있습니다.
구조
<button aria-expanded="false" aria-controls="p1">배송 안내</button> ← 상태는 버튼에
<div id="p1" hidden>…</div> ← 패널은 실제로 숨김| 요소 | 역할 | 속성 | 설명 |
|---|---|---|---|
| 펼침 버튼 | button | aria-expanded, (선택) aria-controls | 펼침 상태는 버튼이 갖습니다. |
| 패널 | 특별한 역할 없음 | id, hidden | 접힘 상태에서는 hidden(또는 display:none)으로 접근성 트리에서도 빠집니다. |
버튼이 콘텐츠 구역의 제목 역할을 한다면 <h3><button>…</button></h3>처럼 제목 요소 안에 버튼을 넣어 제목 탐색으로도 찾을 수 있게 합니다. 반대로 버튼을 감싸지 않고 제목 요소 자체에 role="button"을 주면 제목 의미가 사라집니다.
키보드 동작
| 키 | 동작 |
|---|---|
| Tab | 펼침 버튼으로 포커스를 옮깁니다. 펼쳐진 패널 안의 링크·버튼은 그 다음 순서가 됩니다. |
| Enter | 패널을 펼치거나 접습니다. |
| Space | 패널을 펼치거나 접습니다. |
펼치거나 접어도 포커스는 버튼에 그대로 남습니다. 패널 안으로 포커스를 강제로 옮기지 않습니다. 네이티브 <button>을 쓰면 위 동작은 모두 기본으로 제공됩니다.
필요한 ARIA
button- 펼침 컨트롤은 버튼입니다.
<button type="button">을 쓰면 역할을 따로 줄 필요가 없습니다. 링크(<a href="#">)로 만들면 “링크”로 안내되어 페이지 이동으로 오해하게 됩니다. aria-expanded- 버튼이 제어하는 패널이 펼쳐졌으면
true, 접혔으면false입니다. 화면낭독기는 이 값으로 “확장됨/축소됨”을 안내합니다. 처음부터"false"를 명시해야 “펼칠 수 있는 버튼”으로 인식됩니다. 패널에 붙이지 않고 버튼에 붙입니다. aria-controls(선택)- 버튼이 어떤 패널을 제어하는지
id로 연결합니다. APG에서 선택 사항이며, 이 값을 활용하는 보조기술은 일부(예: JAWS)입니다. 스크립트에서 패널을 찾는 기준으로 쓰기 좋아 함께 넣는 경우가 많습니다. 패널이 DOM에 없을 때는 존재하지 않는id를 가리키지 않도록 합니다.
숨기는 방법은 hidden으로
접힌 패널은 hidden 속성이나 display:none으로 숨깁니다. aria-hidden="true"만 주거나 height:0·opacity:0으로만 감추면 안의 링크가 여전히 Tab으로 잡혀 “보이지 않는 곳에 포커스가 가는” 문제가 생깁니다. 페이지 내 검색(Ctrl + F)으로 접힌 내용을 찾게 하고 싶다면 hidden="until-found"와 beforematch 이벤트를 검토할 수 있습니다(브라우저 지원 확인 필요). 자세한 내용은 콘텐츠 숨기기를 참고하세요.
직접 해 보기
두 구현을 마우스와 키보드(Tab, Enter, Space)로 각각 조작해 보세요.
잘못된 구현
실시간 코드
클릭 이벤트만 붙인 div입니다. 포커스를 받을 수 없고, 버튼이라는 사실도 펼침 상태도 전달되지 않습니다. ▾ 아이콘의 방향은 화면을 보는 사람에게만 의미가 있습니다.
올바른 구현
평일 오후 2시 이전 주문은 당일 출고하며, 보통 1~2일 안에 도착합니다.
실시간 코드
제목 안의 네이티브 버튼에 aria-expanded를 두고, 패널은 hidden으로 숨깁니다. 아이콘 방향도 aria-expanded 값으로 그립니다.
네이티브 details 요소
교환·반품 안내
상품을 받은 날부터 7일 안에 신청할 수 있습니다.
실시간 코드
<summary>를 누르면 브라우저가 open 속성을 스스로 넣고 뺍니다. 스크립트가 전혀 필요 없고, 화면낭독기도 펼침 상태를 안내합니다.
전체 코드
<h3>
<button type="button" aria-expanded="false" aria-controls="ship-panel">
<span class="icon" aria-hidden="true"></span>배송 안내
</button>
</h3>
<div id="ship-panel" hidden>
<p>평일 오후 2시 이전 주문은 당일 출고하며, 보통 1~2일 안에 도착합니다.</p>
</div>
<script>
document.querySelectorAll('button[aria-expanded][aria-controls]').forEach(initDisclosure);
</script>// 디스클로저: 버튼의 aria-expanded와 패널의 hidden을 항상 함께 바꿉니다.
function initDisclosure(button) {
var panel = document.getElementById(button.getAttribute('aria-controls'));
function set(open) {
button.setAttribute('aria-expanded', String(open));
panel.hidden = !open;
}
// 네이티브 <button>이므로 click 하나로 마우스·Enter·Space를 모두 처리합니다.
button.addEventListener('click', function () {
set(button.getAttribute('aria-expanded') !== 'true');
});
set(button.getAttribute('aria-expanded') === 'true'); // 초기 상태 맞추기
}/* 아이콘 방향을 상태에서 바로 읽습니다 */
button[aria-expanded] .icon::before { content: "▸"; display: inline-block; margin-right: .4em; }
button[aria-expanded="true"] .icon::before { transform: rotate(90deg); }<details>
<summary>교환·반품 안내</summary>
<p>상품을 받은 날부터 7일 안에 신청할 수 있습니다.</p>
</details><div class="toggle" onclick="toggle()">배송 안내 ▾</div>
<div class="panel" aria-expanded="false" style="height:0;overflow:hidden">…</div><button type="button" aria-expanded="false" aria-controls="p1">배송 안내</button>
<div id="p1" hidden>…</div>화면낭독기에서는
| 상황 | 들리는 안내 (예) |
|---|---|
| 접힌 버튼에 포커스 | 배송 안내, 버튼, 축소됨 |
| Enter로 펼침 | 확장됨 |
| 펼친 뒤 아래 화살표로 읽기 | 평일 오후 2시 이전 주문은 … |
| 제목 탐색(H)으로 이동 | 배송 안내, 버튼, 축소됨, 제목 수준 3 |
클릭만 붙인 div를 가상 커서로 읽을 때 | 배송 안내 (버튼·상태 안내 없음) |
<details>의 <summary>에 포커스 | 교환·반품 안내, 버튼, 축소됨 (브라우저에 따라 “요약” 등으로 안내) |
점검 체크리스트
- 펼침 컨트롤이
<button>(또는<summary>)이고, Tab·Enter·Space로 조작됩니다. aria-expanded는 패널이 아니라 버튼에 있고, 처음부터"true"/"false"로 명시되어 있습니다.- 화면에 보이는 펼침 상태와
aria-expanded값이 항상 일치합니다. - 접힌 패널은
hidden/display:none으로 숨겨져, 안의 링크가 Tab으로 잡히지 않습니다. - 펼친 패널이 DOM 순서상 버튼 바로 뒤에 있어, 펼친 뒤 Tab을 누르면 패널 안으로 자연스럽게 이어집니다.
- 펼치고 접을 때 포커스가 버튼에 그대로 남습니다.
- 화살표 아이콘은
aria-hidden="true"이거나 CSS로만 그려져 이름에 섞이지 않습니다. - 제목 역할을 하는 버튼은 제목 요소 안에 있습니다.
관련 WCAG 성공 기준
- 1.3.1 정보와 관계 (A) — 버튼과 패널의 관계, 제목 구조가 마크업으로 표현되어야 합니다.
- 1.3.2 의미 있는 순서 (A) — 패널이 버튼 바로 뒤에 와야 읽는 순서가 자연스럽습니다.
- 2.1.1 키보드 (A) — 키보드로 펼치고 접을 수 있어야 합니다.
- 2.4.3 포커스 순서 (A) — 접힌 패널 안의 요소가 Tab 순서에 남아 있으면 안 됩니다.
- 4.1.2 이름, 역할, 값 (A) — 버튼 역할과 펼침 상태(aria-expanded)가 보조기술에 전달되어야 합니다.