フォーム・入力
スライダー
解説ありSlider
値を範囲から選ぶ。aria-valuenow/min/max とキーボード操作。
スライダーとは?
スライダーは、つまみを左右(または上下)に動かして、 ある範囲の中から1つの値を選ぶ UI です。音量・明るさ・再生位置などでよく使われます。
ポイントは、「いまの値」「動かせる範囲(最小・最大)」を、目で見えない人にも伝えること、 そしてマウスだけでなくキーボードでも操作できることです。
なぜアクセシビリティが大事なの?
- キーボードだけで操作する人。つまみが
<div>だとTab でフォーカスできず、矢印キーで値も変えられません。マウス前提の実装は「操作不能」になります。 - スクリーンリーダーを使う人。役割(スライダー)・現在値・範囲が伝わらないと、 「いま何%なのか」「あとどれだけ動かせるのか」が全く分かりません。
この両方を一番カンタンに満たすのが、ネイティブの<input type="range"> です。キーボード操作も読み上げも、ブラウザが用意してくれます。
ライブデモ(推奨実装)
下はネイティブの <input type="range"> です。マウスを使わず、フォーカスして矢印キーで動かしてみてください。
試してみよう:Tab でつまみへ → ← → で1ずつ、Home / End で最小・最大、Page Up / Page Down で大きく動く。
ポイント
スクリーンリーダーでつまみにフォーカスすると「音量, スライダー, 40, 最小 0, 最大 100」のように、役割・現在値・範囲がまとめて読み上げられます。すべてネイティブ要素が自動で提供します。
キーボード操作
| キー | 動作 | 必須/任意 |
|---|---|---|
| → / ↑ | 値を1ステップ増やす | 必須 |
| ← / ↓ | 値を1ステップ減らす | 必須 |
| Home | 最小値にする | 必須 |
| End | 最大値にする | 必須 |
| Page Up / Page Down | 値を大きいステップで増減する | 任意(推奨) |
補足
上のキー操作は、<input type="range"> を使えばすべて自動で手に入ります。 自作(role="slider")する場合は、これらを keydown で自前実装する必要があります。
必要な WAI-ARIA / ロール
| 付ける場所 | 属性 / ロール | 意味 |
|---|---|---|
| ネイティブ range | <label for> で関連付け | つまみに名前(ラベル)を与える。これだけで role・値・範囲は自動。 |
| 自作のつまみ | role="slider" | 「スライダーである」と支援技術へ伝える。 |
| 自作のつまみ | tabindex="0" | div をキーボードでフォーカス可能にする。 |
| 自作のつまみ | aria-valuemin / aria-valuemax | 動かせる範囲(最小・最大)。 |
| 自作のつまみ | aria-valuenow | 現在値。動かすたびに必ず更新する。 |
| 自作のつまみ | aria-valuetext | 数値だけでは意味が伝わらないとき(「40%」「中」など)に人にわかる表現を補う(任意)。 |
| 自作のつまみ | aria-label / aria-labelledby | つまみの名前。可視ラベルがあれば aria-labelledby で結ぶ。 |
実装:推奨パターン(Good)
良い例 / 推奨
まずは <input type="range">。label を結ぶだけで、role・値・範囲・キーボード操作がすべて揃います。
マークアップ:
<label for="volume">音量</label>
<input
type="range"
id="volume"
name="volume"
min="0"
max="100"
step="1"
value="40" />
<output for="volume" id="volume-out">40</output>表示の同期(任意。読み上げは input 自身が担う):
const input = document.getElementById('volume');
const out = document.getElementById('volume-out');
// 値が変わるたびに output へ反映(表示用。読み上げは input が担う)
input.addEventListener('input', () => {
out.textContent = input.value;
});デザイン上どうしてもネイティブが使えないときだけ、role="slider" で自作します。 その場合は値と範囲、キーボード操作をすべて自前で用意します。
<!-- ネイティブが使えない見た目要件のときだけ自作する -->
<span id="bright-label">明るさ</span>
<div
role="slider"
tabindex="0"
aria-labelledby="bright-label"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="40"
aria-valuetext="40%"
class="slider"
id="bright-slider">
<span class="slider-thumb"></span>
</div>const slider = document.getElementById('bright-slider');
const MIN = 0, MAX = 100, STEP = 1, BIG = 10;
function setValue(next) {
const v = Math.min(MAX, Math.max(MIN, next));
slider.setAttribute('aria-valuenow', String(v));
slider.setAttribute('aria-valuetext', v + '%'); // 人にわかる単位を補う
slider.style.setProperty('--pos', ((v - MIN) / (MAX - MIN)) * 100 + '%');
}
slider.addEventListener('keydown', (e) => {
const now = Number(slider.getAttribute('aria-valuenow'));
let next = now;
switch (e.key) {
case 'ArrowRight': case 'ArrowUp': next = now + STEP; break;
case 'ArrowLeft': case 'ArrowDown': next = now - STEP; break;
case 'PageUp': next = now + BIG; break;
case 'PageDown': next = now - BIG; break;
case 'Home': next = MIN; break;
case 'End': next = MAX; break;
default: return; // 関係ないキーは素通り
}
e.preventDefault();
setValue(next);
});アンチパターン(Bad)
下は <div> をマウスでドラッグするだけのスライダーです。マウスでは動きますが、キーボードでは一切操作できず、値も読み上げられません。
試してみよう:Tab を押してもつまみにフォーカスできません。矢印キーも効かず、スクリーンリーダーでは「スライダー」とも現在値とも分かりません。
<!-- ❌ アンチパターン:div をドラッグするだけ -->
<div class="track" onmousedown="startDrag(event)">
<div class="knob" id="knob"></div>
</div>
<!--
role も tabindex もないのでフォーカスできず、
キーボードでは一切動かせない。
aria-valuenow も無いので、いまの値が読み上げられない。
-->悪い例 / 避ける
この実装の問題点:
- キーボードで操作できない —
divはフォーカスを受け取れず、矢印キーで動かせない。 - 役割が伝わらない —
role="slider"が無く、スライダーだと認識されない。 - 値・範囲が伝わらない —
aria-valuenow / valuemin / valuemaxが無く、いま何%かも分からない。 - 名前が無い — 何を調整するスライダーなのか(明るさ?)が伝わらない。
実装チェックリスト
- まず
<input type="range">を検討した(最優先) - つまみに名前がある(
<label for>またはaria-label(ledby)) - キーボードでフォーカスでき、← → で値を変えられる
- Home / End で最小・最大へ動く
- 自作時は
role="slider"+aria-valuemin/max/nowがある - 値を動かすたびに
aria-valuenowを更新している - 数値だけで意味が伝わらないときは
aria-valuetextを添える - フォーカスが見える(フォーカスリングを消していない)