Webパーツ辞典

分割パネルSplit pane

画面を左右(または上下)の2枚のパネルに分け、間の仕切りをつかんで動かすと両側の幅が変わる配置。メールアプリやコードエディターで見かける。

表示例

フォルダ

  • 受信箱
  • 送信済み
  • 下書き

打ち合わせの日程

来週の火曜か水曜の午後でご都合はいかがでしょうか。

アイコン: Font Awesome 7 Free(fa-solid / fa-regular)

別名・英語表記

  • スプリッター
  • リサイズ可能パネル
  • ペイン分割
  • ウィンドウスプリッター
  • ドラッグで幅を変えるパネル
  • 仕切りバー
  • Splitter
  • Split pane

実装例

<div class="x-split-pane">
  <div class="x-split-pane__a" id="x-split-pane-a">
    <div class="x-split-pane__head">
      <p class="x-split-pane__h" id="x-split-pane-h">フォルダ</p>
      <!-- ドラッグできない人のために、1回押すだけで畳める手段も置く -->
      <button class="x-split-pane__fold" type="button" aria-label="フォルダを畳む"><i class="fa-solid fa-angles-left" aria-hidden="true"></i></button>
    </div>
    <ul class="x-split-pane__list">
      <li><i class="fa-solid fa-inbox" aria-hidden="true"></i>受信箱</li>
      <li><i class="fa-regular fa-paper-plane" aria-hidden="true"></i>送信済み</li>
      <li><i class="fa-regular fa-file-lines" aria-hidden="true"></i>下書き</li>
    </ul>
  </div>
  <!-- ドラッグと矢印キーで左の幅を変える。値は左のパネル(主パネル)の幅の割合 -->
  <div class="x-split-pane__sep" role="separator" tabindex="0" aria-orientation="vertical"
       aria-valuenow="40" aria-valuemin="20" aria-valuemax="70" aria-valuetext="40%"
       aria-controls="x-split-pane-a" aria-labelledby="x-split-pane-h">
    <span class="x-split-pane__grip" aria-hidden="true"></span>
  </div>
  <div class="x-split-pane__b">
    <p class="x-split-pane__h">打ち合わせの日程</p>
    <p class="x-split-pane__txt">来週の火曜か水曜の午後でご都合はいかがでしょうか。</p>
  </div>
</div>

AIへの指示文

「分割パネル(Split pane)」を実装してください。

## これは何か
画面を左右(または上下)の2枚のパネルに分け、間の仕切りをつかんで動かすと両側の幅が変わる配置。メールアプリやコードエディターで見かける。

## 使いどころ
- 一覧と詳細、コードとプレビューのように、2つの面を並べて見比べながら作業させたいとき
- どちらの面を広く取りたいかが人や作業によって違い、利用者に幅を決めさせたいとき

## 実装の要件
- display: grid; grid-template-columns: var(--pos) 12px 1fr; にし、JavaScript は変数 --pos だけを書き換える
- ドラッグは pointerdown で setPointerCapture を呼び、pointermove で位置を計算する。仕切りに touch-action: none を付けて、指で動かしたときにページがスクロールしないようにする
- 値を変えるたびに aria-valuenow(必要なら aria-valuetext)も書き換える
- 両側のパネルに min-width: 0 と overflow: auto を付け、狭めたときに中身がはみ出さないようにする
- 幅が固定の左右分割なら 2カラム(layout/two-column)で足りる。仕切りを動かして幅を変えられることが、この部品の要件
- 最小と最大の幅を決める。0 まで縮められると、パネルがあったことに気づけなくなる
- 決めた幅は localStorage などに覚えておく。開くたびに戻ると毎回やり直しになる
- スマートフォンでは左右に並べる幅が無い。狭い画面では縦に積むかタブで切り替え、仕切りは出さない
- ドラッグ中は本文の文字が選択されないよう、つかんでいる間だけ user-select: none にする

## アクセシビリティの要件
- 仕切りは WAI-ARIA APG の Window Splitter の形にする。role="separator" と tabindex="0" でフォーカスできる仕切りにし、aria-valuenow / aria-valuemin / aria-valuemax で主パネルの大きさを数で持つ
- 左右に分ける縦の仕切りには aria-orientation="vertical" を付ける(separator の既定は horizontal)。aria-controls で主パネルを指し、主パネルの名前で aria-labelledby を付ける
- 縦の仕切りは左右の矢印キー、横の仕切りは上下の矢印キーで動かす。Enter で主パネルを畳む/戻す、Home / End で最小/最大へ動かす操作も APG に載っている
- ドラッグだけに頼らない(WCAG 2.2 の 2.5.7 ドラッグ動作)。パネルを畳むボタンや「広げる/狭める」ボタンなど、1回押すだけで幅を変える手段も用意する
- 見える線が細くても、つかめる幅は 24px 相当を確保するか、周りに押せるものを置かない(WCAG 2.2 の 2.5.8 ターゲットのサイズ(最低限))

## 参考にする実装
この形を出発点にして、プロジェクトの命名規則・色・余白に合わせて調整してください。

```html
<div class="x-split-pane">
  <div class="x-split-pane__a" id="x-split-pane-a">
    <div class="x-split-pane__head">
      <p class="x-split-pane__h" id="x-split-pane-h">フォルダ</p>
      <!-- ドラッグできない人のために、1回押すだけで畳める手段も置く -->
      <button class="x-split-pane__fold" type="button" aria-label="フォルダを畳む"><i class="fa-solid fa-angles-left" aria-hidden="true"></i></button>
    </div>
    <ul class="x-split-pane__list">
      <li><i class="fa-solid fa-inbox" aria-hidden="true"></i>受信箱</li>
      <li><i class="fa-regular fa-paper-plane" aria-hidden="true"></i>送信済み</li>
      <li><i class="fa-regular fa-file-lines" aria-hidden="true"></i>下書き</li>
    </ul>
  </div>
  <!-- ドラッグと矢印キーで左の幅を変える。値は左のパネル(主パネル)の幅の割合 -->
  <div class="x-split-pane__sep" role="separator" tabindex="0" aria-orientation="vertical"
       aria-valuenow="40" aria-valuemin="20" aria-valuemax="70" aria-valuetext="40%"
       aria-controls="x-split-pane-a" aria-labelledby="x-split-pane-h">
    <span class="x-split-pane__grip" aria-hidden="true"></span>
  </div>
  <div class="x-split-pane__b">
    <p class="x-split-pane__h">打ち合わせの日程</p>
    <p class="x-split-pane__txt">来週の火曜か水曜の午後でご都合はいかがでしょうか。</p>
  </div>
</div>
```

```css
/* 貼った先のリセットに頼らないよう、この部品の中だけ border-box にする */
.x-split-pane,
.x-split-pane *:not([class*="fa-"]),
.x-split-pane *:not([class*="fa-"])::before,
.x-split-pane *:not([class*="fa-"])::after {
  box-sizing: border-box;
  /* 貼った先の既定の書体に引きずられないようにする。書体が変わると
     文字の幅と高さが変わり、枠のサイズまでずれる */
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Hiragino Sans", "Noto Sans JP", "Yu Gothic UI", Meiryo, sans-serif;
}

/* 左の幅は変数で持ち、ドラッグやキー操作で JavaScript が書き換える */
.x-split-pane {
  --x-split-pane-pos: 40%;
  display: grid;
  grid-template-columns: var(--x-split-pane-pos) 12px 1fr;
  width: 340px;
  max-width: 100%;  /* 狭い画面では親の幅に合わせて縮む */
  height: 170px;
  border: 1px solid #7f8a99;
  border-radius: 8px;
  background: #fff;
  color: #1b2028;
  font-size: 12px;
  overflow: hidden;
}
.x-split-pane__a,
.x-split-pane__b {
  min-width: 0;
  padding: 10px 12px;
  overflow: auto;
}
.x-split-pane__a {
  background: #f3f4f6;
}
.x-split-pane__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 6px;
  margin: -4px 0 4px;
}
.x-split-pane__fold {
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  border: 1px solid #7f8a99;
  border-radius: 5px;
  background: #fff;
  color: #3a4250;
  font-size: 11px;
  cursor: pointer;
}
.x-split-pane__fold:hover {
  background: #e3e8ef;
}
.x-split-pane__fold:focus-visible {
  outline: 2px solid #1d4ed8;
  outline-offset: 1px;
}
.x-split-pane__h {
  margin: 0 0 8px;
  font-size: 12px;
  font-weight: 700;
}
.x-split-pane__head .x-split-pane__h {
  margin: 0;
}
.x-split-pane__list {
  margin: 0;
  padding: 0;
  list-style: none;
}
.x-split-pane__list li {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 5px 0;
  white-space: nowrap;
}
.x-split-pane__list .fa-solid,
.x-split-pane__list .fa-regular {
  width: 14px;
  color: #5b6472;
}
.x-split-pane__txt {
  margin: 0;
  color: #3a4250;
  line-height: 1.6;
}
/* 仕切り。見える線は1本だが、つかめる幅は12px取る */
.x-split-pane__sep {
  position: relative;
  display: grid;
  place-items: center;
  border-left: 1px solid #7f8a99;
  border-right: 1px solid #e3e6ea;
  background: #fff;
  cursor: col-resize;
  touch-action: none;
}
/* つかめる範囲を左右に6pxずつ広げ、24px にする(見た目の幅は変えない) */
.x-split-pane__sep::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: -6px;
  right: -6px;
  z-index: 1;
}
.x-split-pane__grip {
  width: 4px;
  height: 28px;
  border-radius: 2px;
  background: #7f8a99;
}
.x-split-pane__sep:hover .x-split-pane__grip,
.x-split-pane__sep:focus-visible .x-split-pane__grip {
  background: #1d4ed8;
}
.x-split-pane__sep:focus-visible {
  outline: 2px solid #1d4ed8;
  outline-offset: -2px;
}
```

## 出力の約束
- HTML と CSS を分けて出す。JavaScript が要るときは素の JavaScript で書き、UIフレームワークは使わない。
- 参考実装がアイコンに Font Awesome のクラスを使っている場合は、プロジェクトで使っているアイコンかインライン SVG に置き換えてよい。
- 上のアクセシビリティ要件を満たす。満たせない箇所があるときは、理由を添えて先に知らせる。
- 色・角丸・余白は既存のデザイントークンがあればそれを使う。無ければ CSS 変数として切り出す。

使いどころ

  • 一覧と詳細、コードとプレビューのように、2つの面を並べて見比べながら作業させたいとき
  • どちらの面を広く取りたいかが人や作業によって違い、利用者に幅を決めさせたいとき

注意点

  • 幅が固定の左右分割なら 2カラム(layout/two-column)で足りる。仕切りを動かして幅を変えられることが、この部品の要件
  • 最小と最大の幅を決める。0 まで縮められると、パネルがあったことに気づけなくなる
  • 決めた幅は localStorage などに覚えておく。開くたびに戻ると毎回やり直しになる
  • スマートフォンでは左右に並べる幅が無い。狭い画面では縦に積むかタブで切り替え、仕切りは出さない
  • ドラッグ中は本文の文字が選択されないよう、つかんでいる間だけ user-select: none にする

アクセシビリティの要点

  • 仕切りは WAI-ARIA APG の Window Splitter の形にする。role="separator" と tabindex="0" でフォーカスできる仕切りにし、aria-valuenow / aria-valuemin / aria-valuemax で主パネルの大きさを数で持つ
  • 左右に分ける縦の仕切りには aria-orientation="vertical" を付ける(separator の既定は horizontal)。aria-controls で主パネルを指し、主パネルの名前で aria-labelledby を付ける
  • 縦の仕切りは左右の矢印キー、横の仕切りは上下の矢印キーで動かす。Enter で主パネルを畳む/戻す、Home / End で最小/最大へ動かす操作も APG に載っている
  • ドラッグだけに頼らない(WCAG 2.2 の 2.5.7 ドラッグ動作)。パネルを畳むボタンや「広げる/狭める」ボタンなど、1回押すだけで幅を変える手段も用意する
  • 見える線が細くても、つかめる幅は 24px 相当を確保するか、周りに押せるものを置かない(WCAG 2.2 の 2.5.8 ターゲットのサイズ(最低限))

作るときに使えるツール

関連するパーツ