その他

ウィンドウスプリッター

解説あり

Window Splitter

2つの領域の境界をドラッグ/キーで動かすリサイズ用のハンドル。

ウィンドウスプリッターとは?

ウィンドウスプリッターは、2つの領域(ペイン)の境界にあるつまみ(ハンドル)で、それぞれの大きさを調整できる UI です。 ファイラーの「フォルダ一覧|中身」、エディタの「サイドバー|本文」、 メールアプリの「一覧|本文」などでよく見かけます。

マウスならドラッグで直感的に動かせますが、問題はマウスを使わない人です。 キーボードやスクリーンリーダーの利用者にも「ここは大きさを変えられる境界で、 いまどのくらいの割合か」を伝える必要があります。

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

ドラッグ前提で作ると、次の人たちが取り残されます。

解決策は、ハンドルを role="separator"tabindex="0" にして、aria-valuenow / valuemin / valuemax で現在値を伝え、 矢印キーでリサイズできるようにすることです。

ライブデモ(推奨実装)

下の仕切りは APG に沿った実装です。マウスを使わず、境界のハンドルにフォーカスして矢印キーで動かしてみてください。

アクセシブルなウィンドウスプリッター
ナビゲーション
メインコンテンツ

試してみよう:Tab で境界のハンドルへ → ← → で幅を増減 → Home / End で最小・最大 → Enter で折りたたみ/復帰。

ポイント

スクリーンリーダー(macOSなら +F5 で VoiceOver)をオンにすると、 ハンドルにフォーカスしたとき「ナビゲーションの幅, スプリッター, 30」のように名前・役割・現在値が読み上げられます。矢印キーで動かすたびに数値も読み上げられ、 これが aria-valuenow の効果です。

キーボード操作

キー動作必須/任意
/ ペインを狭く / 広くする(縦の境界の場合。横なら / 必須
Home最小サイズにする必須
End最大サイズにする必須
Enter折りたたむ/元のサイズに戻す(任意・推奨)任意
Tab次 / 前のフォーカス可能要素へ移動必須

補足

境界が縦線(左右のペインを区切る)なら左右矢印、横線(上下のペインを区切る)なら上下矢印を使います。aria-orientation は境界線そのものの向きを表し、縦線なら vertical です。

必要な WAI-ARIA / ロール

付ける場所属性 / ロール意味
ハンドルrole="separator"「大きさを変えられる区切り」だと支援技術へ伝える。
ハンドルtabindex="0"div をフォーカス可能にし、Tab で到達できるようにする。
ハンドルaria-valuenow="30"現在の割合(やサイズ)。操作のたびに必ず更新する。
ハンドルaria-valuemin / aria-valuemax動かせる範囲の最小・最大。値はこの範囲にクランプする。
ハンドルaria-controls="ペインのid"どのペインの大きさを操作するかを示す。
ハンドルaria-label または aria-labelledby「何の幅か」を表す名前。フォーカス可能なので名前は必須。
ハンドルaria-orientation="vertical"境界線の向き。縦線なら vertical(既定は horizontal)。

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

良い例 / 推奨

ハンドルを role="separator"tabindex="0" にし、aria-valuenow を見た目(幅)と同期させます。

マークアップ:

<div class="splitter">
  <!-- 左ペイン(リサイズ対象) -->
  <div id="primary-pane" class="pane">
    ナビゲーション
  </div>

  <!-- 境界ハンドル:これがウィンドウスプリッター -->
  <div role="separator"
       tabindex="0"
       aria-orientation="vertical"
       aria-label="ナビゲーションの幅"
       aria-controls="primary-pane"
       aria-valuenow="30"
       aria-valuemin="10"
       aria-valuemax="80"></div>

  <!-- 右ペイン(残りを埋める) -->
  <div class="pane">
    メインコンテンツ
  </div>
</div>

リサイズのスクリプト(aria-valuenow と幅の同期がすべて):

const splitter = document.querySelector('.splitter');
const handle = splitter?.querySelector('[role="separator"]');
const primary = document.getElementById('primary-pane');

if (splitter instanceof HTMLElement && handle instanceof HTMLElement && primary) {
  const MIN = Number(handle.getAttribute('aria-valuemin')); // 10
  const MAX = Number(handle.getAttribute('aria-valuemax')); // 80
  const STEP = 5;
  let prev = Number(handle.getAttribute('aria-valuenow')); // 折りたたみ前の値を覚える

  // aria-valuenow(%)と実際の見た目を同期させるのが肝
  const apply = (value) => {
    const v = Math.min(MAX, Math.max(MIN, Math.round(value)));
    handle.setAttribute('aria-valuenow', String(v));
    primary.style.flexBasis = v + '%';
  };

  handle.addEventListener('keydown', (e) => {
    const now = Number(handle.getAttribute('aria-valuenow'));
    switch (e.key) {
      case 'ArrowLeft':  apply(now - STEP); break;   // 左ペインを狭く
      case 'ArrowRight': apply(now + STEP); break;   // 左ペインを広く
      case 'Home':       apply(MIN); break;          // 最小
      case 'End':        apply(MAX); break;          // 最大
      case 'Enter':                                  // 折りたたみ ⇄ 復帰
        if (now > MIN) { prev = now; apply(MIN); }
        else { apply(prev > MIN ? prev : MAX); }
        break;
      default: return; // 関係ないキーは素通り
    }
    e.preventDefault();
  });
}

補足

マウスのドラッグ対応は追加で付ければ十分です。 まずキーボードと aria-valuenow を確実に動かし、 ドラッグはその上に重ねる、という順番で考えると壊れにくくなります。

アンチパターン(Bad)

下は <div> の仕切りに mousedown でドラッグだけ付けた「見た目だけ同じ」スプリッターです。マウスでは動きますが、キーボードでは一切操作できません。上のデモと同じように Tab を試して、違いを体感してください。

div で作ったドラッグ専用スプリッター
ナビゲーション
メインコンテンツ

試してみよう:Tab を押しても境界にフォーカスが当たりません。スクリーンリーダーでは「区切り」とも「現在30%」とも伝わらず、リサイズできること自体に気づけません。

<!-- ❌ アンチパターン:ドラッグ専用の div 仕切り -->
<div class="splitter">
  <div id="left" class="pane">ナビゲーション</div>

  <!-- role も tabindex も aria-value* も無い。ただの div -->
  <div class="divider" onmousedown="startDrag()"></div>

  <div class="pane">メインコンテンツ</div>
</div>

<script>
  // マウスのドラッグでしか動かない(キーボードは完全に無視)
  function startDrag() {
    document.onmousemove = (e) => {
      document.getElementById('left').style.flexBasis = e.clientX + 'px';
    };
    document.onmouseup = () => (document.onmousemove = null);
  }
</script>

悪い例 / 避ける

この実装の問題点:

  • 区切りだと伝わらないrole="separator" が無く、操作対象に見えない。
  • 現在値・範囲が伝わらないaria-valuenow / valuemin / valuemax が無く、いま何%かも限界も分からない。
  • フォーカスできないtabindex が無く、Tab で到達できない。
  • キーボードでリサイズできない — 矢印・Home/End 処理が無く、マウス専用。
  • 折りたたみできないEnter 等の代替手段が無い。

ポイント

どうしても <div> を使うなら、role="separator"tabindex="0"・ 名前(aria-label)・aria-valuenow/min/max・矢印キーの処理をすべて自前で足す必要があります。ドラッグだけでは半分しか作っていません。

実装チェックリスト


原文(英語):Window Splitter Pattern — W3C APG(新しいタブで開きます)