컴포넌트 패턴
라디오 그룹
여러 선택지 중 하나만 고르는 라디오 버튼 묶음입니다. 묶음 전체가 Tab 한 번으로 들어가고, 안에서는 화살표 키로 이동하며 선택하는 로빙 tabindex 방식을 씁니다.
언제 쓰나요
라디오 그룹은 서로 배타적인 선택지 중 정확히 하나를 고를 때 씁니다. 한 항목을 선택하면 같은 묶음의 다른 항목은 자동으로 해제됩니다. 배송 방법, 결제 수단, 크기 선택처럼 선택지가 2~7개 정도로 적고, 모든 선택지를 한눈에 보여 주는 편이 좋을 때 알맞습니다.
네이티브 라디오 버튼을 먼저 쓰세요
같은 name을 가진 <input type="radio">를 <fieldset>과 <legend>로 묶으면 화살표 키 이동, 하나만 선택, 묶음 이름 안내가 모두 브라우저에서 제공됩니다. 디자인 때문에 직접 만들어야 할 때만 radiogroup과 radio 역할을 씁니다.
다음 경우에는 다른 패턴이 더 알맞습니다.
- 여러 개를 동시에 고를 수 있다면 체크박스를 씁니다.
- 선택지가 많아 공간이 부족하다면
<select>나 리스트박스, 콤보박스를 검토합니다. - 선택하면 아래 내용이 바뀌는 화면 전환이라면 탭이 의미에 맞습니다.
구조
- 묶음 요소에
radiogroup역할을 주고, 보이는 제목을aria-labelledby로 연결해 이름을 줍니다. 보이는 제목이 없으면aria-label을 씁니다. - 각 선택지에
radio역할을 주고, 선택 상태를aria-checked(true/false)로 알립니다. 선택된 항목은 묶음 안에서 하나뿐입니다. - 각 라디오의 이름은 내용 텍스트에서 가져오거나
aria-labelledby로 연결합니다. - 로빙 tabindex: 묶음 안에서 Tab 순서에 들어가는 항목은 하나뿐입니다. 선택된 라디오가
tabindex="0", 나머지는tabindex="-1"입니다. 아무것도 선택되지 않았다면 첫 번째 라디오가tabindex="0"을 가집니다. - 꼭 골라야 하는 항목이면
radiogroup에aria-required="true"를 줍니다.
<h3 id="ship-title">배송 방법</h3>
<div role="radiogroup" aria-labelledby="ship-title">
<div role="radio" aria-checked="false" tabindex="-1">일반 배송</div>
<div role="radio" aria-checked="true" tabindex="0">빠른 배송</div>
<div role="radio" aria-checked="false" tabindex="-1">매장 수령</div>
</div>키보드 동작
| 키 | 동작 |
|---|---|
| Tab / Shift + Tab | 묶음 안팎으로 포커스를 옮깁니다. 묶음에 들어오면 선택된 라디오로, 선택된 항목이 없으면 첫 번째 라디오로 이동합니다(선택된 항목이 없는 네이티브 라디오 묶음에 Shift + Tab으로 들어오면 브라우저에 따라 마지막 항목으로 가기도 합니다). |
| Space | 포커스된 라디오가 선택되지 않았다면 선택합니다. |
| ↓ / → | 다음 라디오로 포커스를 옮기고 선택합니다. 마지막 항목에서는 첫 항목으로 돌아갑니다. |
| ↑ / ← | 이전 라디오로 포커스를 옮기고 선택합니다. 첫 항목에서는 마지막 항목으로 돌아갑니다. |
툴바 안의 라디오 그룹은 예외입니다
라디오 그룹이 toolbar 안에 있으면 화살표 키가 툴바 항목 사이 이동에 쓰이므로, APG는 화살표로 포커스만 옮기고 선택은 Space(또는 Enter)로 하도록 권장합니다. 일반 폼의 라디오 그룹은 위 표처럼 화살표가 곧 선택입니다.
필요한 ARIA
| 역할·속성 | 붙이는 곳 | 설명 |
|---|---|---|
radiogroup | 묶음 | 라디오 묶음임을 알립니다. 네이티브는 <fieldset>(그룹)이 비슷한 역할을 합니다. |
aria-labelledby 또는 aria-label | 묶음, 각 라디오 | 묶음 이름(“배송 방법”)을 줍니다. 라디오 안에 텍스트가 있으면 그 텍스트가 라디오 이름입니다. |
radio | 각 선택지 | 라디오 버튼임을 알립니다. 반드시 radiogroup 안에 둡니다. |
aria-checked | 각 role="radio" | true/false. 직접 만든 라디오에서 필수입니다. 네이티브 라디오에는 쓰지 않고 checked를 씁니다. |
aria-required | 묶음 | 반드시 하나를 골라야 함을 알립니다. 선택 사항입니다. |
aria-describedby | 묶음, 각 라디오 | 보충 설명(예: “빠른 배송은 3,000원 추가”)을 연결합니다. 선택 사항입니다. |
aria-disabled | 각 라디오 | 고를 수 없는 선택지임을 알립니다. 화살표 이동 시 건너뜁니다. |
직접 해 보기
왼쪽은 클릭만 처리하는 잘못된 구현이고, 오른쪽은 로빙 tabindex를 적용한 올바른 구현입니다. 오른쪽 묶음에 Tab으로 들어간 뒤 화살표 키를 눌러 보세요. 포커스와 선택이 함께 움직이고, tabindex 값이 선택된 항목으로 옮겨 가는 것을 실시간 코드에서 확인할 수 있습니다. 다시 Tab을 누르면 묶음 밖으로 한 번에 나갑니다.
잘못된 구현
역할·상태·포커스가 없어 키보드로 고를 수 없고, 화면낭독기는 어느 항목이 선택되었는지 알 수 없습니다.
올바른 구현: 배송 방법
실시간 코드
전체 코드
네이티브 라디오 버튼
<p>배송 방법</p>
<input type="radio" name="ship" id="s1"> <label for="s1">일반 배송</label>
<input type="radio" name="ship" id="s2"> <label for="s2">빠른 배송</label><fieldset>
<legend>배송 방법</legend>
<input type="radio" name="ship" id="s1"> <label for="s1">일반 배송</label>
<input type="radio" name="ship" id="s2" checked> <label for="s2">빠른 배송</label>
<input type="radio" name="ship" id="s3"> <label for="s3">매장 수령</label>
</fieldset>네이티브 라디오는 같은 name끼리 묶여 화살표 이동과 단일 선택을 브라우저가 처리하므로 JavaScript가 필요 없습니다.
직접 만든 라디오 그룹
<h3 id="ship-title">배송 방법</h3>
<div role="radiogroup" id="ship" aria-labelledby="ship-title">
<div role="radio" aria-checked="false" tabindex="-1">일반 배송</div>
<div role="radio" aria-checked="true" tabindex="0">빠른 배송</div>
<div role="radio" aria-checked="false" tabindex="-1">매장 수령</div>
</div>var group = document.getElementById('ship');
var radios = Array.prototype.slice.call(group.querySelectorAll('[role="radio"]'));
function check(target, moveFocus) {
radios.forEach(function (r) {
var on = r === target;
r.setAttribute('aria-checked', String(on));
r.tabIndex = on ? 0 : -1; // 로빙 tabindex: 선택된 항목만 0
});
if (moveFocus) target.focus();
}
radios.forEach(function (r, i) {
r.addEventListener('click', function () { check(r, true); });
r.addEventListener('keydown', function (e) {
var last = radios.length - 1;
switch (e.key) {
case 'ArrowDown':
case 'ArrowRight':
check(radios[i === last ? 0 : i + 1], true); break; // 끝에서 처음으로
case 'ArrowUp':
case 'ArrowLeft':
check(radios[i === 0 ? last : i - 1], true); break; // 처음에서 끝으로
case ' ':
check(r, false); break;
default:
return;
}
e.preventDefault();
});
});선택 시 바로 다른 화면으로 이동하지 마세요
화살표 키로 이동하는 순간 선택이 바뀌므로, 라디오 선택에 페이지 이동이나 큰 화면 변경을 묶으면 키보드 사용자는 원하는 항목에 닿기 전에 맥락이 바뀝니다(WCAG 3.2.2 입력 시). 선택 결과는 별도의 “적용”·“다음” 버튼으로 반영합니다.
화면낭독기에서는
| 상황 | 들리는 안내 (예) |
|---|---|
| Tab으로 묶음에 들어옴 | 배송 방법, 그룹, 빠른 배송, 라디오 버튼, 선택됨, 3개 중 2 |
| ↓로 다음 항목 이동 | 매장 수령, 라디오 버튼, 선택됨, 3개 중 3 |
| ↓로 끝에서 처음으로 돌아감 | 일반 배송, 라디오 버튼, 선택됨, 3개 중 1 |
| 잘못된 구현(div) | 일반 배송 빠른 배송 매장 수령(텍스트로만 읽히고 선택 상태·위치를 알 수 없음) |
점검 체크리스트
- 가능하면 같은
name의 네이티브<input type="radio">와<fieldset>/<legend>를 썼습니다. - 묶음에
role="radiogroup"과 접근 가능한 이름이 있습니다. - 각 라디오에
role="radio"와aria-checked가 있고,true인 항목은 하나뿐입니다. - 묶음 전체가 Tab 순서에서 한 자리만 차지합니다(선택된 항목만
tabindex="0"). - Tab으로 들어오면 선택된 항목, 없으면 첫 항목에 포커스가 갑니다.
- 화살표 키로 포커스와 선택이 함께 이동하고, 양 끝에서 반대쪽 끝으로 돌아갑니다.
- Space로 포커스된 항목을 선택할 수 있습니다.
- 선택 상태가 색만이 아니라 점 모양으로도 구분되고, 포커스 표시가 분명합니다.
- 선택을 바꾸는 것만으로 페이지 이동 같은 맥락 변경이 일어나지 않습니다.
관련 WCAG 성공 기준
- 1.3.1 정보와 관계 (A): 묶음과 묶음 이름을 마크업으로 전달합니다.
- 1.4.11 비텍스트 대비 (AA): 라디오 테두리와 선택 표시가 3:1 이상 대비됩니다.
- 2.1.1 키보드 (A)
- 2.4.3 포커스 순서 (A): 묶음 안팎의 포커스 이동이 예측 가능합니다.
- 2.4.7 포커스 표시 (AA)
- 3.2.2 입력 시 (A): 선택만으로 맥락이 바뀌지 않습니다.
- 4.1.2 이름, 역할, 값 (A)