# ドラッグでしかサイズを変えられない分割パネル

Source: https://easeweb.dev/learn/window-splitter
Topics: ドラッグ＆ドロップ > サイズ変更できるパネルをキーボードで調整できる (WCAG 2.1.1, 2.5.7, 4.1.2)
The fix: 区切りをフォーカスできるseparatorにして、幅を伝え、矢印キー、Home、Endで動くようにしたうえで、「狭く」「広く」のボタンを加えます。または、ドラッグだけの区切りはそのまま残して支援技術から隠し、ボタンだけを加えます。separatorだけでは2.5.7を満たしません。ドラッグできない人には、クリックやタップで幅を変える方法がないからです。
Test with: keyboard, screen reader, mouse, touch, zoom
References: https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/ https://www.w3.org/TR/wai-aria-1.2/#separator https://www.w3.org/WAI/WCAG22/Understanding/dragging-movements.html https://www.w3.org/WAI/WCAG22/Techniques/failures/F108 https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/separator_role

よくある問題を再現した学習用のサンプルです。実在のクライアントの診断結果ではありません。コードは出発点となる実装例のため、実際の製品でも確認してください。ここでは支援技術でのテスト結果は報告していません。

## 場面

メモアプリの画面は2つのパネルに分かれていて、左のサイドバーにメモの一覧、右に開いたメモがあります。長いメモの題名はサイドバーに収まらずに途中で切れるため、サイドバーは広げられるようになっています。パネルの間には細い線があり、ポインターを重ねるとカーソルが左右の矢印に変わります。ドラッグすれば、サイドバーをウィンドウの5分の1から半分までの好きな幅にできます。ところがTabキーを押すと、フォーカスはサイドバーのメモを順にたどった後、開いたメモの「共有」ボタンへ移ります。線には一度も止まらず、ほかに幅を変える方法もページにはありません。

線を `role="separator"` と `aria-orientation="vertical"` で区切りとしてマークアップしているレイアウトもあります。それでもキーボードでは何も変わりません。Tabキーは同じように線を飛ばし、矢印キーも何もしません。スクリーンリーダーでページを読み進めても、見つかるのはせいぜい区切り線で、幅も、変える方法も伝わりません。

## 確認できる問題

区切りは、スクリプトがポインターイベントで動かす空の `div` です。リンクでもボタンでも入力欄でもなく、`tabindex` もないため、Tabで止まらず、キー操作も受け付けません。サイドバーの幅を変えることはアプリの機能の一つですが、キーボードでは一切できないため、2.1.1を満たしません。

幅を変える方法はドラッグだけなので、2.5.7も満たしません。パネルの大きさを変えるのに、ドラッグは必要ありません。ポインターがどんな道筋をたどったかで結果は変わらないからです。キーボードで操作できるようにするだけでも足りません。2.5.7の解説書は、ドラッグ操作をキーボードで同じようにできても、それで自動的にこの達成基準を満たすわけではないとしています。タッチスクリーンやヘッドポインターを使う人、マウスのボタンを押したままにできない人は、矢印キーを使えないこともあり、クリックやタップで操作できるものが必要です。

区切りはスクリプトで動くコントロールなのに、ロールも名前も値もないため、4.1.2も満たしません。`role="separator"` を付けるだけでは解決しません。ARIAの区切りには2種類あります。`hr` のように見た目の境界を示す静的なものと、フォーカスでき、`aria-valuenow` で位置を示すウィジェットです。フォーカスできない区切りは静的なほうになるため、このロールは支援技術に「ここには操作するものがない」と伝えてしまい、サイドバーの幅を示す値もありません。区切りは指定しない限り横向きなので、`aria-orientation="vertical"` 自体は正しいのですが、この属性は線の向きを示すだけで、コントロールにはなりません。

## 利用者への影響

サイドバーの幅を変えたくなるのは、本文のパネルが狭くて読み書きしにくいときや、サイドバーでメモの題名が切れるときです。どちらも、拡大表示を使う人に一番よく起きます。拡大すると、どのパネルにも収まる内容が減るからです。毎日使うアプリで幅を変えられないと、その不便が一日中続きます。

## 自動チェックとAIの限界

自動チェックに見えるのは、名前の要らない空の `div` か、フォーカスできない、つまり静的で何も要らない区切りです。どちらの修正前も、自動チェックのルールには反しません。axeの `aria-required-attr` は、フォーカスできるのに `aria-valuenow` のない区切りを報告するため、途中までの修正は見つかりますが、問題そのものは見つかりません。EaseWebの診断は、サイズ変更のカーソルが表示され、フォーカスを受け取らない細い帯があると、確認を促す項目を出します。ページのほかの場所で幅を変えられるかは、人が試さないと分からないからです。AIのコーディング支援に区切りのアクセシビリティ改善を頼むと、`tabindex="0"` と `role="separator"` を付けただけで終わることがあります。区切りはTabで止まるようになりますが、何もできず、値もなく、ドラッグせずに幅を変える方法もないままです。

## 修正前：ドラッグでしか動かない区切り

```html
<div class="notes-app">
  <nav class="sidebar" id="sidebar" aria-labelledby="notes-title" style="width: 30%">
    <h2 id="notes-title">メモ</h2>
    <ul>
      <li>
        <a href="/notes/packing-list" aria-current="page">持ち物リスト</a>
      </li>
      …
    </ul>
  </nav>
  <div class="divider"></div>
  <main class="note">
    <h1>持ち物リスト</h1>
    <button type="button">共有</button>
    …
  </main>
</div>
```

```js
const sidebar = document.getElementById('sidebar');
const divider = document.querySelector('.divider');
divider.addEventListener('pointerdown', startResize);
// pointermoveでサイドバーの幅をアプリの20%から50%の間で変え、pointerupで終える
```

- 区切りはフォーカスを受け取らないため、キーボードでは届きません。
- 動かす方法はドラッグだけです。
- ロールも値もないため、ここにコントロールがあることも、サイドバーの幅も、支援技術には伝わりません。

## 修正前：同じ区切りに role="separator" を付けたもの

```html
<div class="notes-app">
  <nav class="sidebar" id="sidebar" aria-labelledby="notes-title" style="width: 30%">
    <h2 id="notes-title">メモ</h2>
    <ul>
      <li>
        <a href="/notes/packing-list" aria-current="page">持ち物リスト</a>
      </li>
      …
    </ul>
  </nav>
  <div class="divider" role="separator" aria-orientation="vertical"></div>
  <main class="note">
    <h1>持ち物リスト</h1>
    <button type="button">共有</button>
    …
  </main>
</div>
```

```js
const sidebar = document.getElementById('sidebar');
const divider = document.querySelector('.divider');
divider.addEventListener('pointerdown', startResize);
// pointermoveでサイドバーの幅をアプリの20%から50%の間で変え、pointerupで終える
```

違いはロールだけです。`tabindex` のない区切りは、セクションの間の線を示す静的なものです。

- フォーカスもキー操作も、受け取らないままです。
- 動かす方法は、ドラッグだけのままです。
- 値もありません。

## 修正後：矢印キーで動く区切りと、「狭く」「広く」のボタン

```html
<div class="notes-app">
  <nav class="sidebar" id="sidebar" aria-labelledby="notes-title" style="width: 30%">
    <h2 id="notes-title">メモ</h2>
    <ul>
      <li>
        <a href="/notes/packing-list" aria-current="page">持ち物リスト</a>
      </li>
      …
    </ul>
    <div class="sidebar-width" role="group" aria-label="サイドバーの幅">
      <button type="button" data-step="-5">狭く</button>
      <button type="button" data-step="5">広く</button>
    </div>
  </nav>
  <div class="divider" role="separator" tabindex="0" aria-orientation="vertical"
    aria-labelledby="notes-title" aria-controls="sidebar"
    aria-valuenow="30" aria-valuemin="20" aria-valuemax="50" aria-valuetext="30%"></div>
  <main class="note">
    <h1>持ち物リスト</h1>
    <button type="button">共有</button>
    …
  </main>
</div>
```

```js
const sidebar = document.getElementById('sidebar');
const divider = document.querySelector('.divider');
const [MIN, MAX] = [20, 50];
const width = () => Number(divider.getAttribute('aria-valuenow'));
// ドラッグ、キー、ボタンのどれで幅を変えても、ここを通る
function setWidth(percent) {
  const to = Math.min(MAX, Math.max(MIN, Math.round(percent)));
  sidebar.style.width = `${to}%`;
  divider.setAttribute('aria-valuenow', to);
  divider.setAttribute('aria-valuetext', `${to}%`);
}
const KEYS = { ArrowLeft: () => width() - 5, ArrowRight: () => width() + 5, Home: () => MIN, End: () => MAX };
divider.addEventListener('keydown', (event) => {
  if (!Object.hasOwn(KEYS, event.key)) return;
  event.preventDefault();
  setWidth(KEYS[event.key]());
});
for (const button of document.querySelectorAll('[data-step]')) {
  button.addEventListener('click', () => setWidth(width() + Number(button.dataset.step)));
}
divider.addEventListener('pointerdown', startResize); // ドラッグ中もsetWidth()を呼ぶ
```

1. `tabindex="0"` で、区切りをTabの順序に入れます。フォーカスできる区切りはウィジェットの種類になり、`aria-valuenow` が必須です。
2. 大きさを変えるペインと同じ「メモ」を `aria-labelledby` で名前にし、`aria-controls` でそのペインを指します。
3. `aria-valuenow`、`aria-valuemin`、`aria-valuemax` でサイドバーの幅をアプリに対する割合で示し、`aria-valuetext` で、その数字がパーセントだと伝えます。
4. 左右の矢印キーで5%ずつ動かし、HomeとEndでサイドバーの最小と最大の幅にします。`preventDefault()` で、矢印キーでページがスクロールしないようにします。
5. 「狭く」と「広く」のボタンを加えます。1回のクリックやタップで幅を変えられ、区切りだけでは得られない、ドラッグしない方法になります。HTML標準のボタンなので、Tabの順序に入り、音声操作では「広くをクリック」と言えます。キーと同じ5%ずつ動くので、キーで届く幅にはすべて届きます。
6. どの方法でも `setWidth()` を通すため、スクリーンリーダーが読む値はいつも画面の幅と同じです。ドラッグは近道として残ります。

これで二つの修正前の例はどちらも直り、2.1.1、2.5.7、4.1.2を満たします。区切りは、ARIA Authoring Practices Guide（APG）のウィンドウスプリッターのパターンに沿うものになりました。ただし、Enterキーでペインを折りたたむ操作は含めていません（「実装の判断」を参照）。

## 修正後：「狭く」「広く」のボタン（ドラッグはそのまま）

```html
<div class="notes-app">
  <nav class="sidebar" id="sidebar" aria-labelledby="notes-title" style="width: 30%">
    <h2 id="notes-title">メモ</h2>
    <ul>
      <li>
        <a href="/notes/packing-list" aria-current="page">持ち物リスト</a>
      </li>
      …
    </ul>
    <div class="sidebar-width" role="group" aria-label="サイドバーの幅">
      <button type="button" data-step="-5">狭く</button>
      <button type="button" data-step="5">広く</button>
    </div>
  </nav>
  <div class="divider" aria-hidden="true"></div>
  <main class="note">
    <h1>持ち物リスト</h1>
    <button type="button">共有</button>
    …
  </main>
</div>
```

```js
const sidebar = document.getElementById('sidebar');
const divider = document.querySelector('.divider');
let width = 30;
// ドラッグとボタンのどちらで幅を変えても、ここを通る
function setWidth(percent) {
  width = Math.min(50, Math.max(20, Math.round(percent)));
  sidebar.style.width = `${width}%`;
}
for (const button of document.querySelectorAll('[data-step]')) {
  button.addEventListener('click', () => setWidth(width + Number(button.dataset.step)));
}
divider.addEventListener('pointerdown', startResize); // ドラッグ中もsetWidth()を呼ぶ
```

1. 区切りはドラッグする人のためにそのまま残し、HTML標準のボタン2つで、同じことを1回のクリック、タップ、キー操作でできるようにします。ドラッグと同じ20%から50%までの範囲を、5%刻みで変えられます。
2. `aria-hidden="true"` で、ドラッグだけの区切りをアクセシビリティツリーから外します。ライブラリーが区切りに `role="separator"` を付ける場合に効きます。スクリーンリーダーで見つかるのはボタンだけになり、変えられるもののない区切りは出てきません。
3. ドラッグもボタンも、同じ `setWidth()` を通します。

最も小さな変更で、2.1.1、2.5.7、4.1.2を満たします。しかし、ボタンには値がないため、幅は読まれません。画面を見ない人には幅はほとんど関係ありませんが、少し見えながらスクリーンリーダーを使う人や、拡大鏡と併用する人は、目で判断するしかありません。キーボード利用者は、見えている線ではなく、サイドバーの中のボタンを探す必要もあります。幅が伝わらないという問題は残るため、これだけでは十分ではありません。

## 修正後：矢印キーで動く区切りだけ

```html
<div class="notes-app">
  <nav class="sidebar" id="sidebar" aria-labelledby="notes-title" style="width: 30%">
    <h2 id="notes-title">メモ</h2>
    <ul>
      <li>
        <a href="/notes/packing-list" aria-current="page">持ち物リスト</a>
      </li>
      …
    </ul>
  </nav>
  <div class="divider" role="separator" tabindex="0" aria-orientation="vertical"
    aria-labelledby="notes-title" aria-controls="sidebar"
    aria-valuenow="30" aria-valuemin="20" aria-valuemax="50" aria-valuetext="30%"></div>
  <main class="note">
    <h1>持ち物リスト</h1>
    <button type="button">共有</button>
    …
  </main>
</div>
```

```js
const sidebar = document.getElementById('sidebar');
const divider = document.querySelector('.divider');
const [MIN, MAX] = [20, 50];
const width = () => Number(divider.getAttribute('aria-valuenow'));
// ドラッグとキーのどちらで幅を変えても、ここを通る
function setWidth(percent) {
  const to = Math.min(MAX, Math.max(MIN, Math.round(percent)));
  sidebar.style.width = `${to}%`;
  divider.setAttribute('aria-valuenow', to);
  divider.setAttribute('aria-valuetext', `${to}%`);
}
const KEYS = { ArrowLeft: () => width() - 5, ArrowRight: () => width() + 5, Home: () => MIN, End: () => MAX };
divider.addEventListener('keydown', (event) => {
  if (!Object.hasOwn(KEYS, event.key)) return;
  event.preventDefault();
  setWidth(KEYS[event.key]());
});
divider.addEventListener('pointerdown', startResize); // ドラッグ中もsetWidth()を呼ぶ
```

1. 区切りを、最初の修正からボタンを除いたウィンドウスプリッターにします。フォーカスを受け取り、名前と幅を伝えます。
2. 矢印キー、Home、Endで、ドラッグと同じ `setWidth()` を通して動かします。

キーボード利用者のための2.1.1と、スクリーンリーダーの利用者のための4.1.2は満たします。ただし、2.5.7は満たしません。タッチスクリーン、ヘッドポインター、押したままにできないマウスの利用者はドラッグするしかなく、音声操作の利用者は、矢印キーを1回押すごとに声で指示しなければなりません。これだけで公開せず、最初の修正のようにボタンを加えてください。

## どの方法を選ぶか

区切りとボタンの組み合わせを使ってください。取りこぼしのない修正はこれだけです。キーボード利用者は見えている線のところで、ARIAのパターンどおりに幅を変えられ、スクリーンリーダーの利用者には幅も伝わり、1回のクリックやタップでも幅を変えられます。コードは一番多くなります。区切りの上の矢印キーをスクリーンリーダーがどう扱うかは製品によって異なるため、ボタンはポインターの代わりであると同時に、その備えにもなります。ボタンだけでも3つの達成基準は満たし、区切りを隠す以外にARIAも要らない最も小さな変更です。ただし、幅は読まれず、ボタンは線から離れた場所にあります。ときどき幅を変える程度のサイドバーで、区切りをスプリッターに作り替えるまでの第一歩と考えてください。区切りだけでも足りないので、ボタンを加えてください。

## 実装の判断

値の単位は何でもかまいません。`aria-valuenow`、`aria-valuemin`、`aria-valuemax` が同じ単位を使い、ドラッグ、キー、ボタンのたびに `aria-valuenow` が変われば十分です。ここで使っているアプリに対する割合は、ウィンドウの大きさが変わっても意味が変わりません。ピクセルの幅でもかまいません。ARIAでは、`aria-valuetext` は数字だけでは値を表せないときに使うものとされています。「30」だけではパーセントかピクセルか分からないため、ここでの修正では単位を付けて「30%」とします。数字だけで伝わる場合は付けなくてかまいません。

実際のレイアウトでは、次の点で区切りの動きが決まります。

- 向き：区切りは、指定しない限り横向きです。左右に並ぶパネルには `aria-orientation="vertical"` を付けて左右の矢印キーを使い、一覧の下にプレビューがあるような上下に並ぶパネルは、既定の向きのまま上下の矢印キーを使う。
- 右から左へ書くレイアウト：APGでは、左矢印キーで区切りが左へ動く。サイドバーが右にあると、これでサイドバーが広がるため、ページの向き（`getComputedStyle(divider).direction`）を読み取り、キーの向きを合わせる。
- 折りたたみ：APGのパターンには、Enterキーでペインを折りたたみ、元に戻す操作もあるが、ここでの修正には含めていない。加える場合は、「狭く」「広く」のボタンと同じ理由で、目に見える折りたたみボタンも加え、閉じている間も最小の値が幅として読まれてしまうため、`aria-valuetext` で閉じていることを伝える（「折りたたみ中」）。
- フォーカス：区切りは細いため、ドラッグを受け取る帯全体を囲むフォーカスの枠を描き、両側のパネルに対して3:1以上にする。詳しくは「[コントラストが足りない入力欄・アイコン・フォーカス枠](/ja/learn/non-text-contrast)」を参照。
- ドラッグ：ドラッグを受け取る帯は、描いた線より広くし、`touch-action: none` を付けて、帯の上のドラッグでページがスクロールしないようにする。
- 狭いウィンドウ：400%の拡大表示やスマートフォンでは、「[狭い画面で横スクロールが出るページ](/ja/learn/reflow)」で説明しているように、パネルを縦に並べるか、サイドバーを引き出し式にする。そのときは区切りとボタンを `display: none` で隠し、何の幅も変えないTabの停止位置を残さない。
- 幅の記憶：次に開いたときも幅を保つなら、最初に変えたときだけでなく、ページを読み込んだ時点で、保存した幅から `aria-valuenow` と `aria-valuetext` を設定する。

パネルのライブラリーはさまざまです。つまみに `role="separator"` と `tabindex="0"` を付けても `aria-valuenow` を付けないもの（axeが報告します）、`role="slider"` を付けるもの（矢印キーで動き、値もありますが、2つのパネルの間の線ではなくスライダーとして読まれます）、何も付けないものがあります。ライブラリーの説明ではなく、フォーカスしたつまみが何を伝えるか、クリックだけで幅を変えられるかを確認してください。

## 修正の確認

1. マウスを使わずにTabキーでアプリの中を移動し、区切りか、幅を変えるボタンに、読む順序で到達できるか確認する。
2. キーボードだけで、サイドバーを最も狭く、次に最も広くし、元に戻す。区切りの上の矢印キー、Home、End、またはボタンを使う。フォーカスの枠が見え続け、ページがスクロールしないことを確認する。
3. スクリーンリーダーを使い、Tabキーで区切りへ移って、名前と幅が読まれること、矢印キーを押すたびに幅が読まれることを確認する。矢印キーでページの先が読まれてしまう場合は、ボタンで幅を変えられることを確認する。ボタンだけの修正では幅は読まれず、それがこの修正に残る問題である。
4. マウスのクリックだけで、またタッチスクリーンのタップだけで、ドラッグせずに幅を変える。音声操作では、ボタンの表示テキストを言って幅を変える。
5. 400%の拡大表示か、幅320 CSSピクセルのウィンドウで、パネルが縦に並ぶかサイドバーが閉じ、何の幅も変えないコントロールが残っていないことを確認する。

OS、ブラウザーと支援技術のバージョン、ビルド、実施日、各手順の実際の結果を記録してください。レイアウトやパネルのライブラリーを変更した際は、再度確認します。

## 制限

このサンプルは、どのアプリケーションでも同じ結果になることを保証するものではありません。スクリーンリーダーによって、区切りの読み方や、矢印キーを区切りに渡すかどうかが異なるため、利用者が使う製品で確認してください。価格や音量のような1つのスライダーは「[ドラッグでしか動かない範囲スライダー](/ja/learn/sliders)」、カードや項目の移動は「[マウスでしか動かせないドラッグ＆ドロップのボード](/ja/learn/drag-and-drop)」、ジェスチャー全般は「[ボタンで代われないピンチ・ドラッグ・傾きのジェスチャー](/ja/learn/gestures)」で扱っています。表の列幅の変更、ドッキングや切り離しのできるパネル、3つ以上並ぶペインには、それぞれ別の配慮が必要で、ここでは扱いません。対象とする支援技術で、レイアウト全体を確認してください。

## スクリーンリーダーでの動き（EaseWebが実機で確認）

`role="separator" tabindex="0"`: フォーカス可能な、2つの領域の間の区切りです。ARIAでは、aria-valuenowで位置を示すウィジェットになり、ページのスクリプトが矢印キーで動かします。

| スクリーンリーダー | ブラウザーとOS | Tabで移動 | 矢印キー |
| --- | --- | --- | --- |
| NVDA | Chrome, Windows 11 | 未確認 | 未確認 |
| JAWS | Chrome, Windows 11 | 未確認 | 未確認 |
| VoiceOver | Safari, iOS | 未確認 | 未確認 |
| TalkBack | Chrome, Android | 未確認 | 未確認 |

https://easeweb.dev/ja/learn/screen-reader-support

---

このガイドをコーディングエージェントで使う: MCP https://easeweb.dev/mcp（設定は https://easeweb.dev/ja/services/developers）
