ARIA 접근성 가이드

컴포넌트 패턴

슬라이더

정해진 범위 안에서 손잡이를 움직여 값을 고르는 단일 슬라이더입니다. aria-valuenow·aria-valuemin·aria-valuemax로 값을, aria-valuetext로 사람이 읽기 좋은 값을 알립니다.

언제 쓰나요

슬라이더는 볼륨, 밝기, 온도처럼 최솟값과 최댓값 사이의 연속된 범위에서 값을 고를 때 씁니다. 정확한 숫자보다 대략적인 위치가 중요할 때 알맞습니다. 손잡이(thumb)를 끌거나, 포커스한 뒤 화살표 키로 한 단계씩 움직입니다.

네이티브 슬라이더를 먼저 쓰세요

<input type="range">는 키보드 조작, 포인터 끌기, 값 안내를 모두 제공하며 slider 역할을 이미 가집니다. min, max, step, value가 각각 aria-valuemin, aria-valuemax, 단계, aria-valuenow로 전달됩니다. 숫자만으로는 뜻이 부족할 때는 네이티브에도 aria-valuetext를 더할 수 있습니다.

다음 경우에는 다른 방법이 더 알맞습니다.

  • 정확한 숫자를 입력해야 한다면(예: 수량, 금액) <input type="number">나 spinbutton을 쓰거나, 슬라이더 옆에 숫자 입력란을 함께 둡니다.
  • 선택지가 몇 개뿐인 단계(예: 작게·보통·크게)라면 라디오 그룹이 더 명확합니다.
  • 작업 진행률처럼 사용자가 바꿀 수 없는 값은 progressbar나 <progress>, <meter>를 씁니다.

구조

  • 포커스를 받는 손잡이에 slider 역할과 tabindex="0"을 줍니다. 레일(막대)은 꾸밈 요소입니다.
  • aria-valuenow에 현재 값, aria-valuemin과 aria-valuemax에 최솟값·최댓값을 숫자로 씁니다. 슬라이더의 최솟값·최댓값 기본값은 각각 0과 100입니다.
  • 숫자만으로 뜻이 전달되지 않으면 aria-valuetext에 사람이 읽을 문자열(예: “22도”, “7시간 30분”, “보통”)을 줍니다. 화면낭독기는 aria-valuetext가 있으면 숫자 대신 이 값을 읽습니다.
  • 보이는 레이블을 aria-labelledby로 연결하거나 aria-label로 이름을 줍니다.
  • 세로 슬라이더라면 aria-orientation="vertical"을 줍니다. 슬라이더의 기본 방향은 horizontal입니다.
역할과 속성 관계
<span id="temp-label">실내 온도</span>
<div class="rail">
  <div class="thumb" role="slider" tabindex="0"
       aria-labelledby="temp-label"
       aria-valuemin="16" aria-valuemax="30"
       aria-valuenow="22" aria-valuetext="22도"></div>
</div>

키보드 동작

키보드 동작
키동작
→ / ↑값을 한 단계 올립니다.
← / ↓값을 한 단계 내립니다.
Home최솟값으로 설정합니다.
End최댓값으로 설정합니다.
Page Up(선택) 한 단계보다 큰 폭(예: 5단계)으로 올립니다.
Page Down(선택) 한 단계보다 큰 폭으로 내립니다.

방향과 키의 관계

가로·세로 슬라이더 모두 →와 ↑는 값을 올리고 ←와 ↓는 값을 내립니다. 오른쪽에서 왼쪽으로 쓰는 언어(RTL) 환경에서는 가로 슬라이더의 ←·→ 방향을 반대로 할 수 있습니다. 키를 처리할 때는 preventDefault()로 페이지 스크롤을 막습니다.

필요한 ARIA

필요한 ARIA
역할·속성붙이는 곳설명
slider손잡이범위 안의 값을 고르는 입력임을 알립니다.
aria-valuenow손잡이현재 값(숫자). 직접 만든 슬라이더에서 필수입니다.
aria-valuemin손잡이최솟값(숫자). 생략하면 0입니다.
aria-valuemax손잡이최댓값(숫자). 생략하면 100입니다.
aria-valuetext손잡이사람이 읽기 좋은 값. 숫자에 단위·의미가 필요할 때 씁니다. 값이 바뀔 때마다 함께 갱신합니다.
aria-labelledby 또는 aria-label손잡이슬라이더 이름. 필수입니다.
aria-orientation손잡이세로 슬라이더일 때 vertical. 기본값은 horizontal입니다.
aria-disabled손잡이조작할 수 없는 상태임을 알립니다.

숫자만 바꾸고 valuetext를 그대로 두지 마세요

aria-valuetext를 한 번 넣으면 화면낭독기는 이 값을 우선해서 읽습니다. aria-valuenow만 갱신하고 aria-valuetext를 갱신하지 않으면 손잡이를 움직여도 처음 값이 계속 읽힙니다.

직접 해 보기

왼쪽은 마우스로만 움직이는 잘못된 구현입니다. 가운데는 slider로 만든 올바른 구현이고, 오른쪽은 네이티브 <input type="range">에 aria-valuetext를 더한 예입니다. 손잡이에 Tab으로 이동한 뒤 화살표, Home, End, Page Up, Page Down을 눌러 보세요.

잘못된 구현

실내 온도22

포커스를 받을 수 없고 역할·값이 없어 키보드와 화면낭독기로는 쓸 수 없습니다.

올바른 구현: role="slider"

실내 온도

네이티브: input type="range"

전체 코드

비권장비권장: 마우스 전용 div
<span>실내 온도</span>
<div class="rail" onpointerdown="drag(event)">
  <div class="thumb"></div>
</div>
권장권장: 네이티브 range
<label for="sleep">하루 수면 시간</label>
<input type="range" id="sleep" min="0" max="12" step="0.5"
       value="7.5" aria-valuetext="7시간 30분">
직접 만든 슬라이더 (HTML)
<span id="temp-label">실내 온도</span>
<div class="rail" id="rail">
  <div class="thumb" id="thumb" role="slider" tabindex="0"
       aria-labelledby="temp-label"
       aria-valuemin="16" aria-valuemax="30"
       aria-valuenow="22" aria-valuetext="22도"></div>
</div>
직접 만든 슬라이더 (JavaScript)
var thumb = document.getElementById('thumb');
var rail = document.getElementById('rail');
var min = Number(thumb.getAttribute('aria-valuemin'));
var max = Number(thumb.getAttribute('aria-valuemax'));
var step = 1, bigStep = 5, dragging = false;

function setValue(v) {
  v = Math.min(max, Math.max(min, Math.round(v / step) * step));
  thumb.setAttribute('aria-valuenow', String(v));
  thumb.setAttribute('aria-valuetext', v + '도');   // 숫자와 함께 꼭 갱신
  thumb.style.left = ((v - min) / (max - min) * 100) + '%';
}

thumb.addEventListener('keydown', function (e) {
  var v = Number(thumb.getAttribute('aria-valuenow'));
  switch (e.key) {
    case 'ArrowRight': case 'ArrowUp':   v += step; break;
    case 'ArrowLeft':  case 'ArrowDown': v -= step; break;
    case 'PageUp':   v += bigStep; break;
    case 'PageDown': v -= bigStep; break;
    case 'Home': v = min; break;
    case 'End':  v = max; break;
    default: return;
  }
  e.preventDefault();          // 페이지 스크롤 방지
  setValue(v);
});

// 포인터(마우스·터치·펜)로 끌기
function fromPointer(e) {
  var r = rail.getBoundingClientRect();
  var ratio = Math.min(1, Math.max(0, (e.clientX - r.left) / r.width));
  setValue(min + ratio * (max - min));
}
rail.addEventListener('pointerdown', function (e) {
  dragging = true;
  rail.setPointerCapture(e.pointerId);
  thumb.focus();               // 포인터로 조작해도 포커스를 손잡이에 둡니다
  fromPointer(e);
});
rail.addEventListener('pointermove', function (e) { if (dragging) fromPointer(e); });
rail.addEventListener('pointerup', function () { dragging = false; });
네이티브 range의 aria-valuetext 갱신
var range = document.getElementById('sleep');
range.addEventListener('input', function () {
  var v = Number(range.value);
  var h = Math.floor(v), m = Math.round((v - h) * 60);
  range.setAttribute('aria-valuetext', h + '시간' + (m ? ' ' + m + '분' : ''));
});

끌기 말고 다른 방법도 주세요

WCAG 2.2의 2.5.7 끌기 동작 기준은 끌기로만 할 수 있는 기능에 단일 포인터 대안을 요구합니다. 위 예시처럼 레일의 원하는 위치를 한 번 눌러도 값이 바뀌게 하거나, 옆에 −/+ 버튼이나 숫자 입력란을 둡니다.

화면낭독기에서는

화면낭독기 안내 예시 (표현은 화면낭독기·브라우저·버전에 따라 조금씩 다릅니다)
상황들리는 안내 (예)
슬라이더에 포커스실내 온도, 슬라이더, 22도
→로 한 단계 올림23도
End로 최댓값30도
aria-valuetext 없이 네이티브 range에 포커스하루 수면 시간, 슬라이더, 7.5 (단위 없이 숫자만)
잘못된 구현Tab으로 도달할 수 없고, 읽기 모드에서는 실내 온도 22처럼 텍스트만 읽힘

점검 체크리스트

  • 가능하면 네이티브 <input type="range">와 <label>을 썼습니다.
  • 포커스를 받는 손잡이에 role="slider"와 tabindex="0"이 있습니다.
  • aria-valuenow, aria-valuemin, aria-valuemax가 숫자이며 화면 위치와 일치합니다.
  • 단위나 의미가 필요한 값에 aria-valuetext가 있고, 값이 바뀔 때마다 함께 갱신됩니다.
  • 슬라이더에 접근 가능한 이름이 있습니다.
  • 화살표, Home, End로 값을 바꿀 수 있고, 페이지가 스크롤되지 않습니다.
  • 세로 슬라이더에는 aria-orientation="vertical"이 있습니다.
  • 끌기 외에 한 번 누르기나 버튼 같은 단일 포인터 대안이 있습니다.
  • 손잡이와 레일이 배경과 3:1 이상 대비되고, 포커스 표시가 분명합니다.

관련 WCAG 성공 기준

참고 자료