ボトムシートとは

ボタンを押すと画面の下からスライドして出てくるシート。背景が暗転し、背景クリックか閉じるボタンで戻る。

概要

ボタンを押すと、画面の下からせり上がるようにスライドして出てくるパネルです。スマホアプリでよく使われる表現で、 開いている間は背景を暗転させ、モーダルと同じように操作をそのパネルに集中させます。背景をクリックするか、閉じるボタンを押すと、下にスライドして閉じます。

別名

表記ゆれ:ボトムシート、下からのシート、bottom sheet

Android の Material Design で定義されている部品名としても使われ、そのまま「ボトムシート」という呼び方が定着しています。

AI への伝え方

「ボトムシートを実装して」で通じます。モーダルとの違い(下からスライドして出る)を明示すると、意図した動きになります。 指でドラッグして閉じる操作まで実装してほしいかどうかも、必要なら伝えておくと仕様がぶれません。

似たパーツとの違い

「モーダル」は画面の中央に表示されるのに対し、ボトムシートは画面下端から出てくる点が異なります。スマホでは親指の届く位置に操作を集めたいときにボトムシートが好まれます。 「ドロワーメニュー」は左右の横からスライドして出るのに対し、ボトムシートは下から出るという方向の違いがあります。

デモ

コード

HTML
<div class="bottom-sheet-demo">
  <button class="bottom-sheet-demo__open" type="button" data-sheet-open>
    シートを開く
  </button>

  <div class="bottom-sheet-demo__overlay" id="bottom-sheet-demo-overlay" hidden>
    <div class="bottom-sheet-demo__panel" role="dialog" aria-modal="true" aria-labelledby="bottom-sheet-demo-title">
      <div class="bottom-sheet-demo__handle" aria-hidden="true"></div>
      <div class="bottom-sheet-demo__head">
        <h2 id="bottom-sheet-demo-title">並び替え</h2>
        <button class="bottom-sheet-demo__close" type="button" data-sheet-close aria-label="閉じる">×</button>
      </div>
      <ul class="bottom-sheet-demo__list">
        <li><button type="button">新着順</button></li>
        <li><button type="button">人気順</button></li>
        <li><button type="button">価格が安い順</button></li>
      </ul>
    </div>
  </div>
</div>
CSS
.bottom-sheet-demo {
  font-family: sans-serif;
}

.bottom-sheet-demo__open {
  padding: 10px 20px;
  border: 0;
  border-radius: 8px;
  background: #0017c1;
  color: #fff;
  font-size: 15px;
  cursor: pointer;
}

.bottom-sheet-demo__open:hover {
  background: #00118f;
}

.bottom-sheet-demo__overlay {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  background: rgba(0, 0, 0, 0.5);
  z-index: 100;
}

/* hidden 属性の既定スタイル(display: none)は、上の display: flex より
   詳細度が同じため後勝ちで上書きされてしまう。hidden のときは明示的に隠す */
.bottom-sheet-demo__overlay[hidden] {
  display: none;
}

.bottom-sheet-demo__panel {
  width: 100%;
  max-width: 400px;
  background: #fff;
  border-radius: 16px 16px 0 0;
  box-shadow: 0 -10px 30px rgba(0, 0, 0, 0.25);
  padding: 8px 20px 24px;
  transform: translateY(100%);
  transition: transform 0.25s ease;
}

.bottom-sheet-demo__overlay--open .bottom-sheet-demo__panel {
  transform: translateY(0);
}

.bottom-sheet-demo__handle {
  width: 36px;
  height: 4px;
  margin: 8px auto 12px;
  border-radius: 2px;
  background: #e6e6e6;
}

.bottom-sheet-demo__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 12px;
}

.bottom-sheet-demo__head h2 {
  margin: 0;
  font-size: 16px;
}

.bottom-sheet-demo__close {
  border: 0;
  background: transparent;
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
  padding: 4px 8px;
}

.bottom-sheet-demo__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.bottom-sheet-demo__list button {
  width: 100%;
  text-align: left;
  padding: 12px 10px;
  border: 0;
  border-radius: 8px;
  background: transparent;
  font-size: 14px;
  cursor: pointer;
}

.bottom-sheet-demo__list button:hover {
  background: #f2f2f2;
}

@media (prefers-reduced-motion: reduce) {
  .bottom-sheet-demo__panel {
    transition: none;
  }
}
JS
const overlay = document.getElementById("bottom-sheet-demo-overlay");

function openSheet() {
  overlay.hidden = false;
  // hidden を外した直後に開いた状態のクラスを付けると、transform の transition が効かない
  // (display: none から flex に変わった直後の描画に間に合わないため)、次のフレームで付与する
  requestAnimationFrame(() => {
    overlay.classList.add("bottom-sheet-demo__overlay--open");
  });
}

function closeSheet() {
  overlay.classList.remove("bottom-sheet-demo__overlay--open");
  // スライドして閉じるアニメーションが終わってから hidden にする
  setTimeout(() => {
    overlay.hidden = true;
  }, 250);
}

document.querySelector("[data-sheet-open]").addEventListener("click", openSheet);
document.querySelector("[data-sheet-close]").addEventListener("click", closeSheet);

overlay.addEventListener("click", (event) => {
  if (event.target === overlay) closeSheet();
});

document.addEventListener("keydown", (event) => {
  if (event.key === "Escape" && !overlay.hidden) closeSheet();
});