ユーザーがドラッグ操作でパネル・ペインの幅や高さを自由に変更できるUIコンポーネント。コードエディター(VSCodeのサイドバーやターミナル分割)・ダッシュボードのパネル・チャットUI(メッセージリストとプレビューの分割)などで使われる。ユーザーが自分の作業スタイルに合わせてUIのレイアウトをカスタマイズできる「適応性の高いUI」を実現する。
この記事を読むと、水平・垂直スプリッターの設計・最小・最大サイズ制約の実装・キーボードでのリサイズ(矢印キー)・ドラッグハンドルのARIA対応・リサイズ状態の永続化が自分でできるようになります。
1. UI例(Preview / Live)
実装で見る(GunjoUI)
この部品を、デザインシステム GUNJO の実装で確かめられます。
2. 定義(Definition)
ユーザーがドラッグ操作でパネル・ペインのサイズを変更できるUIコンポーネント。2つ以上のパネルを スプリッター(ドラッグハンドル) で区切り、スプリッターを動かすことで隣接するパネルのサイズが相互に変化する。
水平スプリッター vs 垂直スプリッター:
| 種類 | 方向 | 変化するもの | カーソル |
|---|---|---|---|
| 水平スプリッター | 左右に動く | 左右のパネル幅 | col-resize |
| 垂直スプリッター | 上下に動く | 上下のパネル高さ | row-resize |
3. 使い分け(When to use / When NOT to use)
3.1 When to use
- コードエディター:ファイルツリー + エディター + ターミナルの分割
- ダッシュボード:サイドパネル + メインコンテンツ + インスペクターパネル
- チャット・メール:リスト + プレビューの左右分割
- ユーザーが作業スタイルに合わせてレイアウトをカスタマイズしたいすべての場面
3.2 When NOT to use
- モバイルのみのUI:タッチでのドラッグリサイズは操作が困難でUXが悪い
- レイアウトが固定で良いシンプルなページ
- コンテンツが少なくリサイズの必要がない場合
4. 設計判断の核(Decision Principles)
Resizableの核は「制約の設計」——最小・最大サイズの制約がないと、パネルを潰しすぎてコンテンツが消え、ユーザーが元に戻せなくなる。
判断の優先順位:① 最小・最大サイズ制約 → ② キーボードでのリサイズ対応 → ③ ARIA設定 → ④ リサイズ状態の永続化
- 必ず
minWidth/minHeightを設定する:制約なしだと0pxまでパネルを縮めてコンテンツが消え、ユーザーが操作方法を見失う。最小サイズはコンテンツが最低限読める幅(80〜120px)に設定する - ドラッグハンドルをキーボードで操作できるようにする:
tabIndex={0}を付与し、onKeyDownでArrowLeft/Right/Up/Downキーでの操作を実装する。Shift + Arrowで大きく動かすと操作性が向上する role="separator"+aria-valuenowでARIA対応:スプリッターはrole="separator"で意味を伝え、aria-valuenow(現在サイズ)・aria-valuemin(最小)・aria-valuemax(最大)を設定する。スクリーンリーダーが現在のパネルサイズを読み上げられる- リサイズ状態を
localStorageに保存する:ユーザーがカスタマイズしたサイズはページ遷移・再読み込み後も維持されることを期待する。localStorageにキーを設定して永続化する
5. 状態設計(States)
| 状態 | 表示 |
|---|---|
| 通常 | ハンドルが薄い色で表示 |
| ホバー | ハンドルが強調色に変化・カーソル変更 |
| ドラッグ中 | ハンドルが青くなる・user-select: none |
| フォーカス | focus-visible リング |
| 最小/最大到達 | それ以上動かせないことをユーザーに伝える(ハンドルを非強調化) |
6. バリエーション設計(Variants)
| バリアント | 分割数 | 使用例 |
|---|---|---|
| 2ペイン(左右) | 2 | ファイルツリー + エディター |
| 2ペイン(上下) | 2 | エディター + ターミナル |
| 3ペイン | 3 | ファイルツリー + エディター + インスペクター |
| ネスト | 複数 | VSCode風の複雑なレイアウト |
7. パターン集(Good / Bad / How to fix)
7.1 Bad(典型3つ)
- 最小サイズ制約なしでパネルを0pxまで縮められ、コンテンツが消えてしまう
- ドラッグハンドルに
tabIndexがなくキーボードでリサイズできない - ドラッグ中に
user-select: noneが設定されておらず、テキストが誤選択される
7.2 Good(対になる3つ)
minWidth={80}/maxWidth={400}で操作範囲を制約し、コンテンツが常に読める状態を保つtabIndex={0}+onKeyDownで矢印キーによるリサイズを実装し、キーボードユーザーも操作できるonMouseDown開始時にdocument.body.style.userSelect = 'none'を設定し、onMouseUpで解除する
7.3 How to fix(手順)
minWidth/minHeight/maxWidth/maxHeightを設定する- ハンドルに
tabIndex={0}+onKeyDownで矢印キー操作を実装する role="separator"+aria-orientation+aria-valuenow+aria-valuemin+aria-valuemaxを付与するlocalStorage.setItem('panel-width', leftWidth)でリサイズ状態を永続化する
8. ルール(Must / Better)
Must(守らないと壊れる)
Better(品質が跳ねる)
9. アクセシビリティ要件(必須)
Keyboard
Tabでハンドルにフォーカス←/→(水平)・↑/↓(垂直)で1pxずつリサイズShift + Arrowで大きく(10〜20px)リサイズHome/Endで最小・最大サイズにリセット(任意)
Screen Reader
<div
role="separator"
aria-orientation="vertical"
aria-label="左右パネルの境界線(ドラッグでリサイズ)"
aria-valuenow="200"
aria-valuemin="80"
aria-valuemax="400"
tabIndex="0"
/>
10. 実装メモ(Implementation Notes)
- shadcn/ui の
ResizablePanelGroup/ResizablePanel/ResizableHandleはreact-resizable-panelsライブラリのラッパー。defaultSizeはパーセンテージ(0〜100)で指定し、minSize/maxSizeも同様 react-resizable-panelsではパネルサイズをonLayoutコールバックで取得でき、localStorageへの保存と組み合わせてレイアウトを永続化できる- カスタム実装の場合、
pointermove+pointerdownイベントを使うとタッチデバイスにも対応できる(mousemove+mousedownの代わり)
11. 関連リンク
- 関連するUIデザイン原則: 情報密度 (Information Density)
- 用語集(定義): レスポンシブデザイン (Responsive Design)
- 関連するUIコンポーネント(横): Scroll Area(スクロールエリア), Sidebar(サイドバー), Floating Panel(フローティングパネル), Responsive Patterns(レスポンシブパターン)
12. まとめ
Resizableの設計で最重要なのは「制約の設計(最小・最大サイズ)」と「キーボード操作のサポート」です。迷ったら 4. 設計判断の核 に戻り、「minWidth / minHeight が設定されているか」「矢印キーでリサイズできるか」「role="separator" + aria-valuenow があるか」の3点を確認してください。ドラッグだけで実装したResizableはキーボードユーザーとスクリーンリーダーユーザーを完全に排除してしまいます。