컴포넌트 패턴
슬라이더
정해진 범위 안에서 손잡이를 움직여 값을 고르는 단일 슬라이더입니다. 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
| 역할·속성 | 붙이는 곳 | 설명 |
|---|---|---|
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을 눌러 보세요.
잘못된 구현
포커스를 받을 수 없고 역할·값이 없어 키보드와 화면낭독기로는 쓸 수 없습니다.
올바른 구현: role="slider"
네이티브: input type="range"
실시간 코드
전체 코드
<span>실내 온도</span>
<div class="rail" onpointerdown="drag(event)">
<div class="thumb"></div>
</div><label for="sleep">하루 수면 시간</label>
<input type="range" id="sleep" min="0" max="12" step="0.5"
value="7.5" aria-valuetext="7시간 30분"><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>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; });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 성공 기준
- 1.4.11 비텍스트 대비 (AA): 손잡이·레일의 대비
- 2.1.1 키보드 (A)
- 2.4.7 포커스 표시 (AA)
- 2.5.7 끌기 동작 (AA): 끌기 없이도 값을 바꿀 수 있습니다.
- 2.5.8 대상 크기(최소) (AA): 손잡이가 24×24 CSS 픽셀 이상이거나 충분한 간격을 가집니다.
- 4.1.2 이름, 역할, 값 (A): 현재 값이 보조기술에 전달됩니다.