画面上に常時浮かんだ状態で表示し続けるパネルUIコンポーネント。Modal・Popoverと異なりバックグラウンドのコンテンツを操作しながら同時に参照・操作できることが特徴。図形編集ツールのプロパティパネル・ビデオ通話のミニプレーヤー(PiP)・AIチャットの補助パネル・デバッグコンソールなどで使われる。
この記事を読むと、Floating PanelとModal・Popoverの根本的な違い・ドラッグ移動の実装・折りたたみ(Collapse)設計・画面外へのはみ出し防止(境界制約)・フォーカス管理が自分でできるようになります。
1. UI例(Preview / Live)
実装で見る(GunjoUI)
この部品を、デザインシステム GUNJO の実装で確かめられます。
2. 定義(Definition)
バックグラウンドのコンテンツを操作できる状態を保ちながら、画面上に常時浮かんで表示し続けるパネルUIコンポーネント。Modal(背景を操作不能にする)・Popover(クリックで開き焦点が移る)・Sheet(画面端からスライドする)と異なり、「常時表示・背景操作可・自由配置」 の3点が特徴。
Modal / Dialog との違い:Modalはaria-modal="true" + フォーカストラップで背景を操作不能にする。Floating Panelは背景を操作できる状態で共存し、フォーカストラップは不要(むしろ付けてはいけない)。
Popover との違い:Popoverはクリックで開き、Escape/外クリックで閉じる一時的なパネル。Floating Panelは明示的に閉じるまで表示し続ける永続的なパネル。
Sheet / Drawer との違い:Sheetは画面端(下/左/右)にアンカーされる。Floating Panelは画面上の任意の位置に自由配置でき、ドラッグで移動できる。
3. 使い分け(When to use / When NOT to use)
3.1 When to use
- 図形編集・デザインツールのプロパティパネル:キャンバスを操作しながら選択オブジェクトのプロパティを同時編集する
- AIチャット補助パネル:メインコンテンツを参照しながらAIに質問・指示する
- ビデオ通話のPicture-in-Picture:メインコンテンツを使いながら通話映像を継続表示する
- デバッグコンソール / ログパネル:アプリを操作しながらリアルタイムログを確認する
- ショッピングカートの常時表示:商品一覧を閲覧しながらカート内容を確認する
3.2 When NOT to use
- 重要な確認・警告(削除確認・決済確認)→ Modal / Alert Dialog を使う(背景を遮断して注意を集中させる)
- モバイルがメイン:スクリーンが小さくFloating Panelがコンテンツを覆う → Sheet / Drawerを使う
- 一時的な補足情報(短い補足)→ Tooltip / Popoverで十分
- コンテンツが多くスクロールが必要:Sheet / Drawer の方が適切
3.3 代替UI(Alternatives)
- 重要な確認・操作 →
Modal Dialog/Alert Dialog - 画面端からのパネル →
Sheet / Drawer - 一時的な補足情報 →
Popover/Tooltip - モバイルでの詳細表示 →
Sheet / Drawer
4. 設計判断の核(Decision Principles)
Floating Panelの核は「背景と共存する補助性」——背景コンテンツの操作を妨げないことが大前提。重要な確認・必須の操作をFloating Panelに入れると、ユーザーがパネルを見落としたまま進んでしまう。
判断の優先順位:① 補助的コンテンツのみ(必須の操作を入れない)→ ② 境界制約(画面外はみ出し防止)→ ③ 折りたたみ対応 → ④ モバイルでのSheet代替
- ドラッグ移動には境界制約を必ず実装する:パネルが画面外にはみ出すと閉じるボタンが見えなくなりパネルを操作できなくなる。
Math.max(0, Math.min(maxX, x))で左右・上下の境界に制約をかける - 折りたたみ(Collapse)ボタンで最小化できるようにする:パネルが邪魔な時にヘッダーだけの最小化状態にできると、閉じなくても邪魔にならない。
aria-expandedでスクリーンリーダーに折りたたみ状態を伝える - モバイルではSheetに切り替える:スクリーン幅が小さい場合(768px未満)はFloating Panelでなく画面下からのSheetを使う。レスポンシブブレークポイントでコンポーネントを切り替える
role="dialog"+aria-labelでパネルの目的を伝える:aria-modal="true"は付けない(背景を操作不能にしてはいけない)。スクリーンリーダーがパネルの存在を認識できるようrole="dialog"とaria-labelを付与する
5. 状態設計(States)
5.1 必須状態(Required)
- Visible(表示中):デフォルト状態。任意の位置に表示
- Closed(閉じた状態):閉じるボタンで非表示
5.2 条件付き状態(Conditional)
- Collapsed(折りたたみ):ヘッダーのみ表示、コンテンツを非表示。
aria-expanded="false" - Dragging(ドラッグ中):
cursor: grabbing、テキスト選択を無効化(user-select: none) - Minimized(最小化):コンパクトなアイコン状態に縮小(PiP等)
5.3 State Gallery
| 状態 | 必須 | 何を伝えるか |
|---|---|---|
| Visible | ✅ | パネルが表示中(常時) |
| Closed | ✅ | パネルを閉じた(再表示手段を提供する) |
| Collapsed | — | コンテンツを折りたたみ中(aria-expanded="false") |
| Dragging | — | 現在ドラッグ移動中 |
6. バリエーション設計(Variants)
用途によってサイズ・機能が異なる。
| バリアント | ドラッグ | 折りたたみ | 使用例 |
|---|---|---|---|
| プロパティパネル | ✅ | — | デザインツール・編集ツール |
| AIチャットパネル | — | ✅ | AI補助チャット |
| PiP(ミニプレーヤー) | ✅ | — | ビデオ通話・メディア再生 |
| デバッグパネル | ✅ | ✅ | 開発ツール・コンソール |
禁止パターン:aria-modal="true" を付与する → 背景がスクリーンリーダーに見えなくなり、Floating Panelが閉じるまで背景コンテンツを操作できなくなる(Modalと同等の動作になる)。
7. パターン集(Good / Bad / How to fix)
7.0 よく崩れる設計パターン(3つ)
- 境界制約がなくパネルが画面外にはみ出す:ユーザーがパネルをドラッグしすぎると閉じるボタンが画面外に消えてパネルを閉じられなくなる
- モバイルでFloating Panelを使う:スマートフォンの小さな画面でFloating Panelがコンテンツを覆い、背景コンテンツを操作できなくなる(Floating Panelの意味がなくなる)
- 重要な確認をFloating Panelに入れる:「本当に削除しますか?」のような確認をFloating Panelで実装すると、背景に隠れて見落とされたまま操作が続行される
7.1 Bad(典型3つ)
- ドラッグ移動の制約なし:
setPosition({ x: e.clientX, y: e.clientY })でパネルが画面外にはみ出す - モバイルでもFloating Panelのまま:スクリーン幅が320pxでもFloating Panelを表示し、背景コンテンツの大半が隠れる
aria-modal="true"を付与:スクリーンリーダーで背景が操作不能になり、Floating Panelを閉じるまで他のコンテンツが「見えない」状態になる
7.2 Good(対になる3つ)
- ドラッグ時に
Math.max(0, Math.min(containerWidth - panelWidth, x))で境界制約を実装し、パネルが常に画面内に収まる useMediaQuery('(max-width: 768px)')でモバイルを検知し、<Sheet>に切り替えるレスポンシブ対応を実装するrole="dialog"+aria-labelのみを付与し(aria-modalは付けない)、スクリーンリーダーに「ダイアログが開いています」とだけ伝える
7.3 How to fix(手順)
- ドラッグ処理に境界制約を追加:
x = Math.max(0, Math.min(containerRef.current.offsetWidth - panelRef.current.offsetWidth, newX)) aria-modal="true"を削除し、role="dialog"+aria-labelだけを残すwindow.innerWidthまたは CSS の@mediaでモバイル幅を検知し、Sheet への切り替え処理を追加する- ドラッグ中は
user-select: noneをドキュメントに付与してテキスト選択を防ぐ(ドラッグ終了時に解除)
8. ルール(Must / Better)
Must(守らないと壊れる)
Better(品質が跳ねる)
9. アクセシビリティ要件(必須)
Keyboard
Tabでパネル内のインタラクティブ要素を移動Escapeでパネルを閉じる(フォーカスをパネルを開いたトリガーに戻す)- ドラッグ移動をキーボードで行う場合:矢印キー(10pxずつ移動)+ Shift で粗い移動(50pxずつ)
Focus
- パネルが開いた時:最初のインタラクティブ要素にフォーカスを移す(任意)またはパネルコンテナ自体にフォーカスする
aria-modal="true"は付けない:付けるとフォーカストラップが発生して背景が操作不能になる- Floating Panel内のコントロールは通常の
Tab順序でフォーカス可能
Screen Reader
<div
role="dialog"
aria-label="プロパティパネル"
>
<!-- パネルコンテンツ -->
<!-- aria-modal は付けない -->
</div>
折りたたみボタンには aria-expanded を付与:
<button aria-expanded="false" aria-label="パネルを展開">▲</button>
Touch / Pointer
- タッチデバイスでのドラッグ移動:
onTouchStart/onTouchMove/onTouchEndでタッチドラッグを実装する - タップ領域(閉じるボタン・折りたたみボタン)は最低44×44px
- モバイル(768px未満)ではFloating PanelをSheetに切り替える
10. 実装メモ(Implementation Notes)
- shadcn/ui には Floating Panel のネイティブコンポーネントがない。
@dnd-kit/coreのuseDraggableを使うと、センサー・境界制約・アクセシビリティ対応のドラッグ移動が実装できる - シンプルなドラッグ実装は
onMouseDown→document.addEventListener('mousemove')→document.addEventListener('mouseup')のパターン(本デモで採用)。pointer-eventsを使うとmousedown→pointermove→pointerupでタッチも同時対応できる - パネル位置の永続化:
useEffectでlocalStorage.setItem('panel-position', JSON.stringify(position))を呼び、初期値をJSON.parse(localStorage.getItem('panel-position'))から読み込む - Framer Motion の
dragprop +dragConstraintsを使うと、アニメーション付きのドラッグ移動と境界制約が宣言的に実装できる(<motion.div drag dragConstraints={containerRef} />)
11. 関連リンク
- 関連するUIデザイン原則: 段階的開示 (Progressive Disclosure), 情報密度の最適化 (Information Density)
- 用語集(定義): アクセシビリティ (Accessibility)
- 関連するUIコンポーネント(横): Modal / Dialog(モーダル), Sheet / Drawer(ドロワー), Popover(ポップオーバー), Tooltip(ツールチップ)
12. まとめ
Floating Panelの設計で最重要なのは「背景と共存する補助性の徹底」と「aria-modal を付けないこと」です。迷ったら 4. 設計判断の核 に戻り、「補助的コンテンツのみか(必須操作が入っていないか)」「ドラッグに境界制約があるか」「aria-modal="true" を付けていないか」の3点を確認してください。モバイル対応はSheetへの切り替えが最もシンプルな解決策です。ModalとFloating Panelの選択基準は「背景を遮断してユーザーの注意を集中させる必要があるか」——必要ならModal、背景操作と共存させたい補助機能ならFloating Panelです。