フォーム・入力

スライダー(複数つまみ)

解説あり

Slider (Multi-Thumb)

価格帯などの範囲選択。つまみ同士の干渉とラベリングが論点。

マルチサム・スライダーとは?

マルチサム(複数つまみ)スライダーは、つまみが2つあり、範囲(下限〜上限)を選ぶ UI です。 「価格帯」「年齢層」「時間帯」などの絞り込みでよく使われます。

難しさは、2つのつまみそれぞれに別々の名前・値・範囲を持たせること、 そして下限が上限を超えないように制約することです。

なぜアクセシビリティが大事なの?

ライブデモ(推奨実装)

下は価格帯を選ぶ2つまみスライダーです。つまみは別々にフォーカスでき、矢印キーで動かせます。下限は上限を超えません。

アクセシブルな価格帯スライダー(2つまみ)
価格帯

2000円 〜 6000円

試してみよう:Tab で「最小価格」→「最大価格」へ → ← → で500円ずつ、Page Up / Down で2000円ずつ、Home / End で端まで。最小は最大を越えられない。

ポイント

各つまみにフォーカスすると「最小価格, スライダー, 2000円」「最大価格, スライダー, 6000円」のように、どちらのつまみか現在値が読み上げられます。aria-label でつまみを区別するのが要点です。

キーボード操作

キー動作必須/任意
Tabつまみ間(最小価格 ↔ 最大価格)を移動必須
/ フォーカス中のつまみを1ステップ増やす必須
/ フォーカス中のつまみを1ステップ減らす必須
Home / Endそのつまみが動ける端まで移動必須
Page Up / Page Down大きいステップで増減任意(推奨)

必要な WAI-ARIA / ロール

付ける場所属性 / ロール意味
各つまみrole="slider"つまみ1つ1つを「スライダー」にする(2つあるなら2つとも)。
各つまみtabindex="0"つまみを個別にキーボードフォーカス可能にする。
各つまみaria-label(個別に)「最小価格」「最大価格」のように区別できる名前を付ける。
各つまみaria-valuenowそのつまみの現在値。動かすたびに更新。
下限つまみaria-valuemax = 上限の現在値下限が上限を越えないよう、可動範囲を動的に締める。
上限つまみaria-valuemin = 下限の現在値上限が下限を下回らないよう、可動範囲を動的に締める。
各つまみaria-valuetext「2000円」など単位付きで読み上げさせる(任意・推奨)。

実装:推奨パターン(Good)

良い例 / 推奨

ネイティブの range は1つまみ専用なので、2つまみは role="slider" のつまみを2つ並べて作ります。 各つまみに個別の aria-label動的な可動範囲を持たせるのがコツです。

マークアップ:

<div class="range" id="price-range">
  <span id="price-label">価格帯(円)</span>

  <!-- 下限のつまみ -->
  <div role="slider"
       tabindex="0"
       aria-label="最小価格"
       aria-valuemin="0"
       aria-valuemax="6000"     <!-- 上限つまみの現在値まで -->
       aria-valuenow="2000"
       aria-valuetext="2000円"
       data-thumb="min"></div>

  <!-- 上限のつまみ -->
  <div role="slider"
       tabindex="0"
       aria-label="最大価格"
       aria-valuemin="2000"     <!-- 下限つまみの現在値から -->
       aria-valuemax="10000"
       aria-valuenow="6000"
       aria-valuetext="6000円"
       data-thumb="max"></div>
</div>

キーボード操作と「互いを越えない」制約:

const MIN = 0, MAX = 10000, STEP = 500, BIG = 2000;
const root = document.getElementById('price-range');
const minThumb = root.querySelector('[data-thumb="min"]');
const maxThumb = root.querySelector('[data-thumb="max"]');

function val(t) { return Number(t.getAttribute('aria-valuenow')); }

function render() {
  // 互いを越えないよう、各つまみの可動範囲を相手の現在値で締める
  minThumb.setAttribute('aria-valuemin', String(MIN));
  minThumb.setAttribute('aria-valuemax', String(val(maxThumb)));
  maxThumb.setAttribute('aria-valuemin', String(val(minThumb)));
  maxThumb.setAttribute('aria-valuemax', String(MAX));

  [minThumb, maxThumb].forEach((t) => {
    t.setAttribute('aria-valuetext', val(t) + '円');
    t.style.setProperty('--pos', ((val(t) - MIN) / (MAX - MIN)) * 100 + '%');
  });
}

function onKey(t) {
  return (e) => {
    const lo = Number(t.getAttribute('aria-valuemin'));
    const hi = Number(t.getAttribute('aria-valuemax'));
    const now = val(t);
    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 = lo; break;
      case 'End':  next = hi; break;
      default: return;
    }
    e.preventDefault();
    // 相手を越えない範囲に収める(min ≤ max を保証)
    t.setAttribute('aria-valuenow', String(Math.min(hi, Math.max(lo, next))));
    render();
  };
}

minThumb.addEventListener('keydown', onKey(minThumb));
maxThumb.addEventListener('keydown', onKey(maxThumb));
render();

アンチパターン(Bad)

下はラベルの無い2つのつまみを、マウスでドラッグするだけのものです。どちらが下限・上限か分からず、キーボードでも操作できません。

ラベル無し・マウス専用の2つまみ
価格帯

試してみよう:Tab でつまみにフォーカスできません。スクリーンリーダーでは2つのつまみが見分けられず、値も範囲も伝わりません。

<!-- ❌ アンチパターン:ラベルの無い2つまみ・マウス専用 -->
<div class="track">
  <div class="knob" onmousedown="drag(event, this)"></div>
  <div class="knob" onmousedown="drag(event, this)"></div>
</div>
<!--
  どちらが「最小価格」「最大価格」か区別できない。
  role / aria-valuenow が無いので値も範囲も読み上げられない。
  tabindex が無いのでキーボードでは触れない。
-->

悪い例 / 避ける

この実装の問題点:

  • どちらのつまみか分からないaria-label が無く「最小」「最大」を区別できない。
  • キーボードで操作できないtabindex が無く、矢印キーも効かない。
  • 値・範囲が伝わらないrole="slider"aria-valuenow が無い。
  • 制約が無い — 下限が上限を追い越しても止まらず、値が壊れる。

実装チェックリスト


原文(英語):Slider (Multi-Thumb) Pattern — W3C APG(新しいタブで開きます)