本文へスキップ

ドラッグ式ボトムシート

画面下からせり出すシート。ハンドルをドラッグして「半分/全開/閉じる」の3段階にスナップする。iOS/AndroidのモバイルUI定番。

プレビュー

ドラッグ

操作ドラッグ/スワイプで操作できます(PC・スマホ対応)。

実装メモ

ハンドルの pointerdown でドラッグを開始し、move/up は document で受けて(指がハンドルから外れても追従)シートの translateY を直接更新、離した瞬間に最寄りのスナップ点(全開0 / 半分120 / 閉じ240px)へ transition で吸着させます。ドラッグ中は transition を切って指に追従、スナップ時だけ効かせるのがなめらかさの要点。ハンドルとシートに touch-action:none を付け、縦ドラッグが枠のスクロールと競合しないようにしています。背景の暗さはシートの開き具合に連動します。

使いどころ

共有シート・アクションメニュー・フィルタなど、モバイルで片手操作を優先したい場面に向く。指でドラッグして開閉度合いを直接コントロールできるため、「今どれくらい開いているか」が視覚的にも触覚的にも明確なのが強み。全開・半分・閉じるの3段階スナップは、地図アプリの詳細パネルのように「少し見たい」「全部見たい」の両方が必要な画面と相性がよい。逆に、PCのマウス操作が主で指でのドラッグが使えない環境では強みが活きないため、デスクトップではモーダルやポップオーバーに切り替える設計が定番になる。ボタン数個程度の軽いアクションリストなら、この可変ドラッグの複雑さは不要でポップオーバーで十分な場合が多い。

仕組み

シートの高さ(H=240)に対し SNAPS=[0,120,H] の3点(全開・半分・閉じ)を定義し、ハンドルの pointerdown で dragging を開始する。setPointerCapture でそのポインタIDの以降の move/up イベントをハンドル自身に固定するため、指がハンドルからシート本体や画面外へ外れても追従が途切れない。ドラッグ中は setT() が startT(開始時のtranslateY)に移動量を足した値へ直接 style.transform を書き換えて指に張り付かせ、このとき .dragging クラスで transition を無効化して遅延なく追従させる。指を離すと nearest() が3つのスナップ点のうち最も近いものを選び、transition を戻した snapTo() で吸着させる。背景の暗さ(backdrop の opacity)は openness = 1 - curT/H の計算でシートの開き具合に連動し、ドラッグ中もリアルタイムに更新される。

実装の注意

最も見落としやすいのは .bs-sheet に touch-action: none を付けている点で、これはハンドルのドラッグとスクロールの取り合いを防ぐには必要だが、同じ値がシート内部の .bs-inner にも及ぶため、リスト項目が増えて中身がシートの高さ(240px)を超えたときに、タッチ操作でリストを縦スクロールできなくなる。デモの5項目はたまたま収まっているが、項目を増やすなら .bs-inner に touch-action: pan-y を明示して上書きし、ハンドルのドラッグとリストのスクロールを両立させる必要がある。また、ドラッグの吸着先は SNAPS の3点固定なので、シートの高さ H や項目数を変えたら SNAPS の数値も連動して書き換えないと、中途半端な位置にスナップするようになる。pointerId を個別に保存せずマルチタッチを考慮していない点も、タブレットでの実運用では問題になりうる。

アクセシビリティ

この実装はドラッグ操作専用で、開閉ともに支援技術・キーボードへの配慮が入っていない。「シートを開く」ボタンだけは button 要素なのでキーボードで押せて半分の位置まで開けるが、そこから全開にする・閉じる操作はハンドル(button ではない div)のドラッグと backdrop のクリックにしか紐づいておらず、キーボード操作だけでは一度開いたシートを閉じる手段が無い。実装するときは、共有パネル・確認ダイアログに近い性質を持つのでモーダルと同じ Dialog パターン(role="dialog" aria-modal="true"、開いたときのフォーカス移動、Escapeで閉じる、閉じるボタンの明示)を土台にし、そのうえでドラッグは「あれば嬉しい追加操作」として重ねるのが安全だ。ハンドルを button 化するか tabindex="0" と keydown(矢印キーで3段階を移動)を足せば、ドラッグできない環境でも同じ体験に届く。

調整して使う

触る値は SNAPS の位置とトランジションのイージングの2つが中心。デモは [全開0, 半分120, 閉じる240] だが、全開を使わず半分/閉じるの2段階だけにすればシンプルな開閉シートになる。cubic-bezier(0.32,0.72,0,1) は「勢いよく動いて最後で減速する」定番の動きの再現を狙ったもので、もっと控えめな印象にしたい画面では standard easing に寄せる方が馴染む。ハンドルの見た目(幅40px・高さ5px・角丸)はつまみやすさと視認性のバランスなので、タップ領域そのものは .bs-handle の padding で44px相当を確保しておくと指の大きさに対して現実的になる。背景の最大暗さ(openness*0.9)を1.0にすると全開時に完全な黒帯になり圧迫感が増すため、0.85〜0.9程度に抑えるのが無難。

コード(コピーして使えます)

<div class="bs-screen" id="bs-screen">
  <div class="bs-content">
    <button class="bs-open" id="bs-open">シートを開く</button>
  </div>
  <div class="bs-backdrop" id="bs-backdrop"></div>
  <div class="bs-sheet" id="bs-sheet">
    <div class="bs-handle" id="bs-handle"><span></span></div>
    <div class="bs-inner">
      <h3>共有する</h3>
      <ul class="bs-list">
        <li><span class="bs-ic">🔗</span>リンクをコピー</li>
        <li><span class="bs-ic">✉️</span>メールで送る</li>
        <li><span class="bs-ic">💬</span>メッセージ</li>
        <li><span class="bs-ic">⭐</span>お気に入りに追加</li>
        <li><span class="bs-ic">⬇️</span>ダウンロード</li>
      </ul>
    </div>
  </div>
</div>

タグ

同じカテゴリ「通知・オーバーレイ」のほかのパーツ

関連パーツ