本文へスキップ
HELLO!
HIGASHIYUKI®.COM
0   /   100
オーバーレイ

ドロワーメニューが左から滑り込み、ページを押しのけて背景が暗くなる

デモ

操作:クリック。 「さわってみる」を押すと自動再生が止まり、マウスで動かせます。

仕様

動作
DOM
種類
レシピ
動作確認
three 0.186.0・gsap 3.15.0
公開日

しくみと調整

「メニュー」を押すと、白いパネルが画面の左の外から滑り込み、後ろのページは同じ動きで右へ少し押しのけられながら、半透明の幕で暗くなります。パネルの中の項目は、パネルを追いかけるように上の段から順に左から寄ってきます。閉じるときは同じタイムラインを逆にたどるので、項目が消えてからパネルが引き、ページが元の位置へ戻ります。ページの上に重ねるだけのドロワーと違い、ページも一緒に動くので、パネルが横から押し出してきたように見えます。

パネルとページは、0.7 秒の power4.out で同時に動かします。出だしが速く、残りの時間をかけてゆっくり止まるので、押された勢いが伝わります。ページを押しのける量は画面の幅の 7.5%(パネルの幅の 4 分の 1 ほど)にとどめ、幕は 0.5 秒で暗くします。項目は 0.15 秒遅れて、0.05 秒ずつずらしながら 24px 左から寄せます。閉じるのは 1.4 倍速の逆再生で、開くより短くして待たせないようにします。

押しのけたページには、閉じたあと transform を残さないようにします。transform の付いた要素の中では、固定ヘッダーのような position: fixed の要素が画面ではなくその要素に付いてしまい、スクロールすると一緒に流れます。閉じたら transform を消し、開くまではページに触らないようにしておきます(ページのトゥイーンだけ、作った時点で描かない設定にします)。

パネルはダイアログの要素に入れて showModal で開きます。焦点の閉じ込め、後ろのページの締め出し、閉じたあとの焦点の戻しまで任せられます。Esc は既定だとその場で消えてしまうので、cancel を止めて同じ動きで閉じます。開いているあいだは後ろのページをスクロールさせず、スクロールバーの幅を先に空けておくと、バーが消えてもページが横にずれません。

押しのけずに重ねるだけにするなら、ページの動きを外すだけで済みます。右から出すなら、パネルの位置と動きの向きを逆にします。PJAX のようにページを読み直さずに移るサイトでは、リンクを押したときにも閉じるようにしておきます。ネットショップのメニューや、項目の多いサイトのナビゲーションに向いています。

コード

HTML
<!-- ページの中身を包む要素。ドロワーが開くと、同じ動きで少し右へ押しのける(押しのけないなら data-drawer-page を外す) -->
<div class="page" data-drawer-page>
  <button class="drawer-open" type="button" aria-haspopup="dialog" aria-controls="site-drawer">
    <span class="drawer-open__bars" aria-hidden="true"></span>メニュー
  </button>
  <!-- ここにヘッダーの残りと、ページの中身が入る -->
</div>

<!-- showModal() で開く <dialog> には、焦点の閉じ込め・背面の締め出し・Esc・焦点の戻しが付いてくる。
     dialog は画面いっぱいに広げ、幕とパネルを中に置く(幕もパネルと一緒に GSAP で動かすため) -->
<dialog class="drawer" id="site-drawer" aria-labelledby="drawer-title">
  <div class="drawer__scrim"></div>
  <nav class="drawer__panel" aria-labelledby="drawer-title">
    <div class="drawer__head">
      <h2 class="drawer__title" id="drawer-title">Menu</h2>
      <!-- 焦点は、開いたとき最初の押せる所(この閉じるボタン)に移る -->
      <button class="drawer__close" type="button" aria-label="閉じる"></button>
    </div>
    <ul class="drawer__links">
      <li><a href="/shop/">オンラインストア</a></li>
      <li><a href="/features/">特集</a></li>
      <li><a href="/stories/">読みもの</a></li>
      <li><a href="/about/">私たちについて</a></li>
      <li><a href="/stores/">店舗一覧</a></li>
    </ul>
    <ul class="drawer__sub">
      <li><a href="/orders/">ご注文の確認</a></li>
      <li><a href="/shipping/">配送と返品</a></li>
      <li><a href="/contact/">お問い合わせ</a></li>
    </ul>
    <a class="drawer__login" href="/login/">ログイン</a>
  </nav>
</dialog>

CSS
.page,
.drawer {
  --ink: #262626; /* 文字と線 */
  --mute: #6b6b6b; /* 補助の項目 */
  --surface: #ffffff; /* パネルの面 */
  --line: rgba(38, 38, 38, 0.12); /* 罫線 */
  --scrim: rgba(17, 17, 17, 0.45); /* 後ろのページを暗くする幕 */
}

/* 開いているあいだは後ろをスクロールさせない。バーの幅を先に空けておき、消えたときにページが横へずれないようにする */
html { scrollbar-gutter: stable; }
html:has(.drawer[open]) { overflow: hidden; }

.drawer-open {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  height: 48px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--ink);
  font: inherit;
  font-size: 14px;
  font-weight: 600;
  letter-spacing: 0.04em;
  cursor: pointer;
}

/* 3 本線(幅 24px・線 2px・間 6px)。上下の線は影で描く */
.drawer-open__bars {
  width: 24px;
  height: 2px;
  border-radius: 1px;
  background-color: currentColor;
  box-shadow: 0 -8px 0 currentColor, 0 8px 0 currentColor;
}

/* dialog を画面いっぱいに広げる(既定の余白・枠・最大の大きさを外す) */
.drawer {
  width: 100%;
  max-width: none;
  height: 100%;
  max-height: none;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  overflow: hidden;
}

.drawer::backdrop { background: none; }
.drawer__scrim { position: absolute; inset: 0; background-color: var(--scrim); }

.drawer__panel {
  position: absolute;
  inset: 0 auto 0 0;
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
  width: min(400px, 100% - 48px);
  padding: 32px 40px;
  overflow-y: auto;
  overscroll-behavior: contain;
  background-color: var(--surface);
  box-shadow: inset -1px 0 0 var(--line); /* 影は付けず、右の縁に罫線 */
  color: var(--ink);
}

.drawer__head { display: flex; flex: none; align-items: center; justify-content: space-between; height: 48px; }
.drawer__title { margin: 0; color: var(--mute); font-size: 13px; letter-spacing: 0.14em; text-transform: uppercase; }

/* アイコンだけのボタンは線の丸。× は線 2 本で描く */
.drawer__close { position: relative; width: 40px; height: 40px; padding: 0; border: 0; border-radius: 50%; box-shadow: inset 0 0 0 1px var(--ink); background: none; cursor: pointer; }
.drawer__close::before,
.drawer__close::after { content: ""; position: absolute; inset: 19px 12px; border-radius: 1px; background-color: var(--ink); rotate: 45deg; }
.drawer__close::after { rotate: -45deg; }

/* 行の罫線は li に引いて動かさず、中の a だけを寄せる */
.drawer__links { margin: 40px 0 0; padding: 0; border-top: 1px solid var(--line); list-style: none; }
.drawer__links li { border-bottom: 1px solid var(--line); }
.drawer__links a { display: flex; align-items: center; justify-content: space-between; min-height: 55px; color: inherit; font-size: 24px; font-weight: 700; letter-spacing: -0.01em; text-decoration: none; }
.drawer__links a::after { content: "→" / ""; color: var(--mute); font-size: 16px; font-weight: 600; }

.drawer__sub { display: grid; gap: 16px; margin: 32px 0; padding: 0; list-style: none; }
.drawer__sub a { color: var(--mute); font-size: 14px; font-weight: 600; letter-spacing: 0.04em; text-decoration: none; }

/* 主ボタン。margin-top: auto でパネルの下端に置く */
.drawer__login { display: grid; flex: none; place-items: center; height: 48px; margin-top: auto; border-radius: 999px; background-color: var(--ink); color: var(--surface); font-size: 14px; font-weight: 600; letter-spacing: 0.04em; text-decoration: none; }

.drawer-open:focus-visible,
.drawer__close:focus-visible,
.drawer a:focus-visible { outline: 2px solid var(--ink); outline-offset: 4px; }

JavaScript
import { gsap } from "gsap";

const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const SPEED = reduceMotion ? 20 : 1; // 動きを減らす設定では、ほぼ一瞬で切り替える
const CLOSE_SPEED = 1.4; // 閉じるほうは少し速く
const PUSH = 7.5; // ページを押しのける量(ページの幅に対する %)

document.querySelectorAll(".drawer-open").forEach((button) => {
  const drawer = document.getElementById(button.getAttribute("aria-controls"));
  const scrim = drawer.querySelector(".drawer__scrim");
  const panel = drawer.querySelector(".drawer__panel");
  const items = panel.querySelectorAll(".drawer__links a, .drawer__sub, .drawer__login");
  const page = document.querySelector("[data-drawer-page]");

  // 閉じる動きが終わってから close() を呼ぶ(先に呼ぶと、その場で消える)
  const open = gsap
    .timeline({ paused: true, onReverseComplete: () => drawer.close() })
    .fromTo(scrim, { opacity: 0 }, { opacity: 1, duration: 0.5, ease: "power2.out" }, 0)
    .fromTo(panel, { xPercent: -100 }, { xPercent: 0, duration: 0.7, ease: "power4.out" }, 0)
    // 中身はパネルを追いかけるように、上から順に左から寄せる
    .fromTo(items, { opacity: 0, x: -24 }, { opacity: 1, x: 0, duration: 0.5, ease: "power3.out", stagger: 0.05 }, 0.15);
  // ページには開くまで触らない(immediateRender: false)。transform が先に付くと、中の固定の要素がずれる
  if (page) open.fromTo(page, { xPercent: 0 }, { xPercent: PUSH, duration: 0.7, ease: "power4.out", immediateRender: false }, 0);

  const close = () => open.timeScale(SPEED * CLOSE_SPEED).reverse();

  button.addEventListener("click", () => {
    drawer.showModal(); // 焦点がパネルの中へ移り、背面は操作できなくなる。閉じると焦点はこのボタンへ戻る
    open.timeScale(SPEED).play();
  });
  drawer.querySelector(".drawer__close").addEventListener("click", close);
  scrim.addEventListener("click", close);
  // PJAX や SPA ではページが読み直されないので、リンクを押したら閉じておく
  panel.addEventListener("click", (event) => {
    if (event.target.closest("a")) close();
  });
  // Esc は既定だとその場で閉じるので、いったん止めて同じ動きで閉じる
  drawer.addEventListener("cancel", (event) => {
    event.preventDefault();
    close();
  });
  // 閉じたら、ページの transform を消す(残すと、ページの中の position: fixed が画面ではなくページに付く)。
  // Esc を続けて押すと、ブラウザが動きを待たずに閉じることがあるので、ここでタイムラインも最初に戻す
  drawer.addEventListener("close", () => {
    open.pause(0);
    if (page) gsap.set(page, { clearProps: "transform" });
  });
});

インタラクションデザインについて、
お気軽にご相談ください。

ここに並べているのは、Web サイトの中で動く UI のサンプルです。新しく作るサイトに組み込むことも、いま公開中のサイトに、ボタンやヒーロー、スクロールの演出だけを足すこともできます。設計から実装までの進め方はWeb サイト制作、端末ごとの見え方や表示速度の考え方はレスポンシブウェブサイトのページにまとめています。映像として作る動きをお探しの場合はモーショングラフィックスをご覧ください。