その他
ウィンドウスプリッター
解説ありWindow Splitter
2つの領域の境界をドラッグ/キーで動かすリサイズ用のハンドル。
ウィンドウスプリッターとは?
ウィンドウスプリッターは、2つの領域(ペイン)の境界にあるつまみ(ハンドル)で、それぞれの大きさを調整できる UI です。 ファイラーの「フォルダ一覧|中身」、エディタの「サイドバー|本文」、 メールアプリの「一覧|本文」などでよく見かけます。
マウスならドラッグで直感的に動かせますが、問題はマウスを使わない人です。 キーボードやスクリーンリーダーの利用者にも「ここは大きさを変えられる境界で、 いまどのくらいの割合か」を伝える必要があります。
なぜアクセシビリティが大事なの?
ドラッグ前提で作ると、次の人たちが取り残されます。
- キーボードだけで操作する人。仕切りが
<div>だとTab でフォーカスできず、そもそもリサイズできません。 - スクリーンリーダーを使う人。ただの
<div>は 「区切り(セパレーター)」とも、「いま 30%」とも認識されず、操作対象だと気づけません。
解決策は、ハンドルを 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 → ← → を試して、違いを体感してください。
試してみよう: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・矢印キーの処理をすべて自前で足す必要があります。ドラッグだけでは半分しか作っていません。
実装チェックリスト
- ハンドルに
role="separator"が付いている - ハンドルが
tabindex="0"でフォーカスできる aria-label/aria-labelledbyで名前が付いているaria-valuenowが操作と常に一致し、見た目の幅と同期しているaria-valuemin/aria-valuemaxがあり、値が範囲内にクランプされるaria-controlsで操作対象のペインを指している- 矢印キーでリサイズ、Home/End で最小・最大にできる
- (任意)Enter で折りたたみ/元のサイズに復帰できる
- キーボードだけで操作でき、フォーカスが見える(フォーカスリングを消していない)