操作可能 / 2.5 入力モダリティ

2.5.7ドラッグ動作

レベル AA2.2 新規

Dragging Movements

ドラッグで行う操作(スライダー・並べ替え・境界の移動など)には、ドラッグを使わない代替手段(クリックやボタン)も用意する。

WCAG 2.2 で新しく追加された達成基準です

この基準は WCAG 2.1 にはなく、2.2 で新設されました。 まだ日本語の解説が少ない領域なので、下記のリンク先とあわせて理解を深めてください。

ドラッグ動作の代替がなぜ必要なの?

スライダーのつまみを引きずる・リストを並べ替える・ペインの境界線を動かすなど、「押しながら移動させる」操作(ドラッグ)は、次のような人には難しいか不可能です。

  • 手が震える人(本態性振戦・パーキンソン病など): 指を押し続けながら正確に動かすことができません。 ワンタップ・ワンクリックで完結するボタン操作なら対応できます。
  • マウス操作が不安定な人・トラックパッドが苦手な人: ドラッグ中にポインタが外れ、操作を途中で失敗することがあります。
  • スイッチデバイスを使う人(1〜2個のスイッチで PC を操作する方): そもそもドラッグ操作自体ができません。

WCAG 2.5.7 は、ドラッグで行えるすべての操作に、ドラッグを使わない代替手段(単一のクリック・タップ、または専用ボタン)も用意することを義務付けています。

補足

「キーボードで操作できる(達成基準 2.1.1)」とは別の要件です。2.5.7 はポインタ(マウス・タッチ)を使いながらもドラッグが難しい人が対象です。 ボタンやトラッククリックなどポインタ操作での代替があれば 2.5.7 を満たせます。 キーボード代替も合わせて提供すると 2.1.1 も同時にカバーできます。

ポイント

例外:お絵かきアプリの手書き・地図の境界線描画など、 ドラッグの軌跡そのものが機能の本質である場合は代替手段は不要です。 スライダー・並べ替え・リサイズハンドルなど「最終的な位置だけが重要」な操作は例外になりません。

不合格の例(ドラッグ専用)

下のスライダーはつまみをドラッグするだけで値を変えます。 +/− ボタンも、トラックのクリックで移動する機能もありません。 手が震える人やスイッチデバイスの利用者は値を正確にセットできません。

ドラッグ専用スライダー(2.5.7 不合格)
音量

つまみはドラッグでしか動きません。ドラッグが難しい人は操作できません。

<!-- ❌ ドラッグ専用:代替手段なし -->
<span class="sv-bad-label">音量</span>
<div class="sv-bad-track">
  <!-- tabindex・role なし → キーボードでフォーカスできない -->
  <div class="sv-bad-thumb" id="bad-knob"></div>
</div>
<!-- +/− ボタンなし、トラッククリックも不可 -->

悪い例 / 避ける

この実装の問題点:

  • ドラッグの代替がない — ボタンもトラッククリックもなく、手が震える人・スイッチ利用者は使えない。
  • キーボードで操作できないrole="slider"tabindex もなく、キー操作不能。
  • 現在値が伝わらないaria-valuenow がなく、スクリーンリーダーに値が読み上げられない。
  • 役割が伝わらないrole="slider" がなく、スライダーだと認識されない。

合格の例(ボタン・クリック・キーボードの代替)

下のスライダーは4通りの方法で操作できます。 ドラッグが難しいユーザーも、自分に合った方法で値を変えられます。

ドラッグ+ボタン+クリック+キーボード対応スライダー(2.5.7 合格)
音量40

ドラッグでも、+/− ボタンでも、トラックのクリックでも、キーボードでも動かせます。

ポイント

スクリーンリーダーでつまみにフォーカスすると「音量、スライダー、40、最小値 0、最大値 100」のように役割・現在値・範囲が読み上げられます。aria-valuenow を 操作のたびに更新しているため、値を変えると支援技術にすぐ伝わります。

キーボード操作

キー動作必須/任意
/ 値を 1 ステップ増やす必須
/ 値を 1 ステップ減らす必須
Home最小値(0)にする必須
End最大値(100)にする必須
Page Up / Page Down値を大きいステップ(10)で増減する任意(推奨)

補足

キーボード操作は WCAG 2.1.1「キーボード」の要件でもあります。2.5.7 が求めるのは ボタン・クリックによるポインタ代替ですが、両方を実装することで幅広いユーザーをカバーできます。<input type="range"> を使えばキーボード操作はブラウザが自動で提供します。

実装(コード)

良い例 / 推奨

ドラッグ(Pointer Events)・トラッククリック・+/− ボタン・キーボードの4つの入力経路を用意し、どれでも同じ setValue() を呼びます。aria-valuenow は操作のたびに必ず更新してください。

マークアップ:

<!-- ドラッグ+クリック+ボタン+キーボードの代替あり -->
<div class="sv-wrap">
  <div class="sv-header">
    <span id="d257-vol-label">音量</span>
    <span id="d257-vol-out" class="sv-val">40</span>
  </div>
  <div class="sv-row">
    <button type="button" class="sv-btn" id="d257-minus"
            aria-label="音量を下げる">−</button>

    <div class="sv-track" id="d257-track">
      <div class="sv-fill" id="d257-fill" style="width:40%"></div>
      <div role="slider"
           tabindex="0"
           id="d257-thumb"
           class="sv-thumb"
           aria-labelledby="d257-vol-label"
           aria-valuemin="0"
           aria-valuemax="100"
           aria-valuenow="40"
           style="left:40%"></div>
    </div>

    <button type="button" class="sv-btn" id="d257-plus"
            aria-label="音量を上げる">+</button>
  </div>
</div>

スクリプト:

const track = document.getElementById('d257-track');
const thumb = document.getElementById('d257-thumb');
const fill  = document.getElementById('d257-fill');
const out   = document.getElementById('d257-vol-out');
const minus = document.getElementById('d257-minus');
const plus  = document.getElementById('d257-plus');

const MIN = 0, MAX = 100, STEP = 1, BIG = 10;

function getValue() {
  return Number(thumb.getAttribute('aria-valuenow'));
}
function setValue(next) {
  const v = Math.min(MAX, Math.max(MIN, Math.round(next)));
  const pct = ((v - MIN) / (MAX - MIN)) * 100;
  // スクリーンリーダーへのフィードバック:操作のたびに必ず更新する
  thumb.setAttribute('aria-valuenow', String(v));
  thumb.style.left = pct + '%';
  fill.style.width  = pct + '%';
  out.textContent   = String(v);
}

/* ① ドラッグ — Pointer Events + setPointerCapture */
// setPointerCapture でタッチ・スタイラスでも指が外れても追従し続ける
thumb.addEventListener('pointerdown', (e) => {
  e.preventDefault();
  thumb.setPointerCapture(e.pointerId);
});
thumb.addEventListener('pointermove', (e) => {
  if (!thumb.hasPointerCapture(e.pointerId)) return;
  const { left, width } = track.getBoundingClientRect();
  setValue(((e.clientX - left) / width) * MAX);
});

/* ② トラッククリックで移動(2.5.7 代替手段) */
track.addEventListener('click', (e) => {
  if (e.target === thumb) return; // つまみのクリックは無視
  const { left, width } = track.getBoundingClientRect();
  setValue(((e.clientX - left) / width) * MAX);
  thumb.focus(); // フォーカスをつまみへ
});

/* ③ +/− ボタン(2.5.7 代替手段) */
minus.addEventListener('click', () => setValue(getValue() - STEP));
plus.addEventListener('click',  () => setValue(getValue() + STEP));

/* ④ キーボード(APG role="slider" 準拠) */
thumb.addEventListener('keydown', (e) => {
  const v = getValue();
  const map = {
    ArrowRight: v + STEP, ArrowUp:   v + STEP,
    ArrowLeft:  v - STEP, ArrowDown: v - STEP,
    PageUp:     v + BIG,  PageDown:  v - BIG,
    Home: MIN,  End: MAX,
  };
  if (!(e.key in map)) return;
  e.preventDefault();
  setValue(map[e.key]);
});

setValue(40); // 初期値を確定して UI と aria を同期

チェックリスト

  • ドラッグで行える操作に、ボタン または クリックの代替手段がある
  • 代替手段はドラッグなしの単一ポインタ操作(クリック・タップ)で完結する
  • 代替手段はドラッグと同等の結果が得られる(精度・範囲)
  • スライダーのつまみに role="slider"tabindex="0" がある
  • aria-valuemin / aria-valuemax / aria-valuenow が正しく設定されている
  • 値を変えるたびに aria-valuenow を更新している
  • キーボードで Home End が機能する(2.1.1 も満たす)
  • つまみに touch-action: none を設定し、スクロールとの競合を防いでいる
  • フォーカスリングを消していない(outline: none 禁止)
  • ドラッグ中に指が外れてもキャプチャが維持される(setPointerCapture
  • ドラッグの軌跡自体が本質的な機能(手書き等)は例外と理解している

原典・規範文書