ARIA 접근성 가이드

컴포넌트 패턴

라디오 그룹

여러 선택지 중 하나만 고르는 라디오 버튼 묶음입니다. 묶음 전체가 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

필요한 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로 묶기
<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가 필요 없습니다.

직접 만든 라디오 그룹

HTML
<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>
JavaScript
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 성공 기준

참고 자료