컴포넌트 패턴
체크박스
켜짐·꺼짐 두 가지 상태를 고르는 이중 상태 체크박스와, 하위 항목 일부만 선택되었음을 알리는 혼합(mixed) 상태 “전체 선택” 체크박스를 만드는 방법입니다.
언제 쓰나요
체크박스는 서로 독립적인 선택지를 하나씩 켜고 끌 때 씁니다. 여러 개를 동시에 고를 수 있고, 아무것도 고르지 않아도 됩니다. 체크박스에는 두 종류가 있습니다.
- 이중 상태(dual-state) 체크박스: 선택됨(
true)과 선택 안 됨(false) 두 상태만 가집니다. 대부분의 체크박스가 여기에 해당합니다. - 삼중 상태(tri-state) 체크박스: 여기에 혼합(
mixed) 상태가 더해집니다. 보통 하위 체크박스 묶음을 한 번에 제어하는 “전체 선택” 체크박스에 쓰며, 하위 항목이 일부만 선택되었을 때 혼합 상태가 됩니다.
네이티브 체크박스를 먼저 쓰세요
<input type="checkbox">는 키보드 조작, 포커스, 상태 안내, 폼 제출을 모두 기본으로 제공합니다. 혼합 상태도 네이티브로 만들 수 있습니다. HTML 속성은 없지만 JavaScript로 input.indeterminate = true를 설정하면 브라우저가 보조기술에 혼합 상태로 전달합니다. 디자인 때문에 직접 만들어야 할 때만 checkbox 역할을 씁니다.
다음 경우에는 다른 패턴이 더 알맞습니다.
- 여러 선택지 중 하나만 고른다면 라디오 그룹을 씁니다.
- 누르는 즉시 설정이 켜지고 꺼지는 기능(예: 알림 켜기)이라면 스위치가 더 의미에 맞습니다.
- “굵게”처럼 눌림 상태가 있는 도구 버튼이라면 토글 버튼(
aria-pressed)을 씁니다.
구조
- 체크박스 요소에
checkbox역할을 줍니다. 네이티브<input type="checkbox">는 이미 이 역할을 가집니다. - 현재 상태를
aria-checked로 알립니다. 값은true,false, 그리고 삼중 상태라면mixed입니다. - 체크박스의 이름은 보이는 레이블에서 가져옵니다. 직접 만든 체크박스는 내용 텍스트로 이름이 정해지거나,
aria-labelledby로 레이블을 연결합니다. 네이티브는<label>로 연결합니다. - 관련된 체크박스 묶음은
<fieldset>+<legend>, 또는group역할과aria-labelledby로 묶어 묶음 이름을 줍니다. - “전체 선택” 체크박스는
aria-controls에 자신이 제어하는 하위 체크박스들의id를 공백으로 나열할 수 있습니다.
<div role="group" aria-labelledby="toppings-title">
<h3 id="toppings-title">토핑</h3>
<div role="checkbox" tabindex="0" aria-checked="mixed"
aria-controls="t1 t2 t3">전체 선택</div>
<input type="checkbox" id="t1"> <label for="t1">치즈</label>
<input type="checkbox" id="t2" checked> <label for="t2">버섯</label>
<input type="checkbox" id="t3"> <label for="t3">올리브</label>
</div>키보드 동작
| 키 | 동작 |
|---|---|
| Space | 포커스된 체크박스의 상태를 바꿉니다. 혼합 상태 체크박스는 아래 순서로 상태가 돌아갑니다. |
| Tab / Shift + Tab | 다음·이전 체크박스로 이동합니다. 체크박스는 묶음 안에서도 각각 Tab 순서에 들어갑니다(라디오 그룹과 다른 점). |
APG 예시의 혼합 상태 “전체 선택” 체크박스는 다음과 같이 순환합니다.
- 혼합(일부 선택) → 누르면 모두 선택(
true) - 모두 선택 → 누르면 모두 해제(
false) - 모두 해제 → 누르면 사용자가 마지막으로 만든 일부 선택 조합으로 복원(
mixed)
마지막 단계에서 되살릴 일부 선택 조합이 없다면 모두 선택으로 바꾸는 것이 일반적입니다. 혼합 상태로 돌아가는 단계를 아예 빼고 “모두 선택 ↔ 모두 해제”만 오가도록 만들어도 됩니다. 어떤 방식이든 하위 체크박스 상태와 aria-checked 값이 항상 일치해야 합니다.
Enter 키는?
APG 체크박스 패턴에서 필수 키는 Space뿐입니다. 네이티브 체크박스도 Enter로는 상태가 바뀌지 않으며, 폼 안에서는 Enter가 폼 제출로 이어질 수 있습니다. 직접 만든 체크박스도 네이티브와 같게 Space로 동작하게 만듭니다.
필요한 ARIA
| 역할·속성 | 붙이는 곳 | 설명 |
|---|---|---|
checkbox | 체크박스 요소 | 체크박스임을 알립니다. 네이티브 <input type="checkbox">에는 필요 없습니다. |
aria-checked | role="checkbox" 요소 | true / false / mixed. 직접 만든 체크박스에서 필수입니다. 네이티브 체크박스에는 쓰지 않고 checked, indeterminate를 씁니다. |
aria-labelledby 또는 aria-label | 체크박스, 묶음 | 보이는 레이블이 다른 요소에 있을 때 연결합니다. 체크박스 안에 텍스트가 있으면 그 텍스트가 이름이 됩니다. |
group | 체크박스 묶음 | <fieldset>을 쓸 수 없을 때 관련 체크박스를 묶고 이름을 줍니다. |
aria-controls | “전체 선택” 체크박스 | 제어하는 하위 체크박스들의 id 목록. 선택 사항입니다. |
aria-describedby | 체크박스 | 추가 설명(예: 약관 요약)을 연결합니다. 선택 사항입니다. |
aria-disabled | 체크박스 | 비활성 상태를 알립니다. 네이티브는 disabled 속성을 씁니다. |
직접 해 보기
왼쪽은 클릭만 처리하는 흔한 잘못된 구현이고, 오른쪽은 혼합 상태를 지원하는 올바른 구현입니다. Tab으로 이동하고 Space로 눌러 보세요. 하위 항목을 하나만 해제하면 “전체 선택”이 혼합 상태로 바뀌는 것을 실시간 코드에서 확인할 수 있습니다.
잘못된 구현
역할·상태·포커스가 없어 키보드로 닿을 수 없고, 화면낭독기는 일반 텍스트로만 읽습니다.
올바른 구현: 피자 토핑
실시간 코드
전체 코드
이중 상태 체크박스
<div class="checkbox" onclick="this.classList.toggle('on')">
이용약관에 동의
</div><input type="checkbox" id="agree">
<label for="agree">이용약관에 동의</label>직접 만들어야 한다면 역할, 상태, 포커스, 키보드를 모두 갖춥니다.
<div role="checkbox" tabindex="0" aria-checked="false" id="agree">
이용약관에 동의
</div>var box = document.getElementById('agree');
function toggle() {
var on = box.getAttribute('aria-checked') === 'true';
box.setAttribute('aria-checked', String(!on));
}
box.addEventListener('click', toggle);
box.addEventListener('keydown', function (e) {
if (e.key === ' ') {
e.preventDefault(); // Space의 기본 동작(스크롤) 막기
toggle();
}
});혼합 상태 “전체 선택”
<fieldset>
<legend>피자 토핑</legend>
<div role="checkbox" id="all" tabindex="0" aria-checked="mixed"
aria-controls="opt1 opt2 opt3">전체 선택</div>
<ul>
<li><input type="checkbox" id="opt1"> <label for="opt1">치즈 추가</label></li>
<li><input type="checkbox" id="opt2" checked> <label for="opt2">버섯</label></li>
<li><input type="checkbox" id="opt3"> <label for="opt3">올리브</label></li>
</ul>
</fieldset>var all = document.getElementById('all');
var items = ['opt1', 'opt2', 'opt3'].map(function (id) {
return document.getElementById(id);
});
var saved = items.map(function (el) { return el.checked; }); // 마지막 일부 선택 조합
function syncAll() {
var n = items.filter(function (el) { return el.checked; }).length;
all.setAttribute('aria-checked',
n === 0 ? 'false' : n === items.length ? 'true' : 'mixed');
}
function toggleAll() {
var state = all.getAttribute('aria-checked');
var partial = saved.some(Boolean) && !saved.every(Boolean);
items.forEach(function (el, i) {
if (state === 'mixed') el.checked = true; // 혼합 → 모두 선택
else if (state === 'true') el.checked = false; // 모두 선택 → 모두 해제
else el.checked = partial ? saved[i] : true; // 모두 해제 → 일부 선택 복원
});
syncAll();
}
all.addEventListener('click', toggleAll);
all.addEventListener('keydown', function (e) {
if (e.key === ' ') { e.preventDefault(); toggleAll(); }
});
items.forEach(function (el) {
el.addEventListener('change', function () {
saved = items.map(function (x) { return x.checked; });
syncAll();
});
});
syncAll();네이티브로 혼합 상태 만들기
“전체 선택”도 네이티브 체크박스로 만들 수 있습니다. 하위 항목이 일부만 선택되면 all.indeterminate = true, 그 밖에는 all.indeterminate = false와 함께 all.checked를 맞춥니다. 사용자가 누르면 브라우저가 indeterminate를 false로 바꾸므로, change 이벤트에서 하위 항목을 함께 갱신합니다.
화면낭독기에서는
| 상황 | 들리는 안내 (예) |
|---|---|
| 묶음 안으로 처음 이동 | 피자 토핑, 그룹 |
| 혼합 상태 “전체 선택”에 포커스 | 전체 선택, 체크 상자, 부분 선택됨 |
| Space를 눌러 모두 선택 | 체크됨 |
| 하위 항목에 포커스 | 버섯, 체크 상자, 체크됨 |
| 잘못된 구현(div)에 도달 | 이용약관에 동의(역할·상태 없이 텍스트로만 읽힘, Tab으로는 도달 불가) |
점검 체크리스트
- 가능하면 네이티브
<input type="checkbox">와<label>을 썼습니다. - 직접 만든 체크박스에
role="checkbox",tabindex="0",aria-checked가 모두 있습니다. - Space로 상태가 바뀌고, 이때 페이지가 스크롤되지 않습니다.
- 모든 체크박스에 보이는 레이블과 같은 접근 가능한 이름이 있습니다.
- 관련 체크박스 묶음에
fieldset/legend또는role="group"과 이름이 있습니다. - “전체 선택”의
aria-checked(또는indeterminate)가 하위 항목 상태와 항상 일치합니다. - 네이티브 체크박스에
aria-checked를 덧붙이지 않았습니다. - 선택·해제·혼합 상태가 색만이 아니라 체크 표시·대시 같은 모양으로도 구분되고, 고대비 모드에서도 보입니다.
- 포커스 표시가 분명하게 보입니다.
관련 WCAG 성공 기준
- 1.3.1 정보와 관계 (A): 묶음과 레이블 관계를 마크업으로 전달합니다.
- 1.4.11 비텍스트 대비 (AA): 체크 표시와 테두리가 배경과 3:1 이상 대비됩니다.
- 2.1.1 키보드 (A): 키보드만으로 상태를 바꿀 수 있습니다.
- 2.4.7 포커스 표시 (AA)
- 3.3.2 레이블 또는 지시 (A)
- 4.1.2 이름, 역할, 값 (A): 역할과 선택 상태가 보조기술에 전달됩니다.