【GSAP】play()とreverse()で超簡単!timelineを使ったハンバーガーメニュー開閉アニメーション

Webサイトのスマートフォン表示や、モダンなデザインのPCサイトでよく見かける「全画面のハンバーガーメニュー」。 メニューが開く際に「背景が広がってから、ナビゲーションの文字が順番にフワッと現れる」といったリッチな演出を入れたい場面がありますよね。

CSSだけでも実装は可能ですが、アニメーションのタイミング(遅延)を計算するのは非常に面倒です。 そこで大活躍するのが、GSAPの timeline 機能です!

timeline を使えば、複数のアニメーションを連結させてひとつの「タイムライン」として管理できます。さらに play()(再生)と reverse()(逆再生)のメソッドを使うことで、開く時も閉じる時も非常にシンプルに制御することが可能です。 今回は、メニュー開閉アニメーションの基本をデモと共にご紹介します!

デモ動作

See the Pen GSAP timelineで作るメニュー開閉 by mikanbako (@mikanbako) on CodePen.

全画面でのデモ動作は以下のリンクからご確認下さい!(別タブで開かれます)

実装コードの紹介

【HTML】

<header class="l-header">
  <div class="l-header__inner">
    <h1 class="l-header__logo">LOGO</h1>
    <!-- ハンバーガーボタン -->
    <button class="c-hamburger js-hamburger" aria-label="メニューを開閉する">
      <span></span>
      <span></span>
      <span></span>
    </button>
  </div>
</header>

<!-- 全画面メニュー -->
<nav class="p-fullscreen-menu js-menu">
  <ul class="p-fullscreen-menu__list">
    <!-- アニメーションで順に表示させるためのクラス(js-menu-link)を付与 -->
    <li class="p-fullscreen-menu__item js-menu-link"><a href="#">Home</a></li>
    <li class="p-fullscreen-menu__item js-menu-link"><a href="#">About</a></li>
    <li class="p-fullscreen-menu__item js-menu-link"><a href="#">Services</a></li>
    <li class="p-fullscreen-menu__item js-menu-link"><a href="#">Contact</a></li>
  </ul>
</nav>

<main class="l-main">
  <p>メインコンテンツがここに入ります。</p>
  <p>右上のボタンをクリックしてね!</p>
</main>

【CSS】

/* ヘッダー周り */
.l-header {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  padding: 20px;
  z-index: 100; /* メニューより手前にボタンを配置するため一番高くする */
}

.l-header__inner {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.l-header__logo {
  font-size: 24px;
  font-weight: bold;
}

/* ハンバーガーボタンのスタイル(今回はシンプルなCSSアニメーションで×印に) */
.c-hamburger {
  position: relative;
  width: 40px;
  height: 40px;
  cursor: pointer;
  z-index: 100;
}

.c-hamburger span {
  position: absolute;
  left: 5px;
  width: 30px;
  height: 2px;
  background-color: #1bb4d4;
  transition: transform 0.3s ease-out, opacity 0.3s ease-out;
}

.c-hamburger span:nth-child(1) { top: 10px; }
.c-hamburger span:nth-child(2) { top: 19px; }
.c-hamburger span:nth-child(3) { top: 28px; }

/* activeクラス付与時(バツ印になる) */
.c-hamburger.is-active span:nth-child(1) {
  transform: translateY(9px) rotate(45deg);
}
.c-hamburger.is-active span:nth-child(2) {
  opacity: 0;
}
.c-hamburger.is-active span:nth-child(3) {
  transform: translateY(-9px) rotate(-45deg);
}

/* 全画面メニュー(GSAPで表示させるため初期状態は非表示) */
.p-fullscreen-menu {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100svh;
  background-color: rgba(255, 255, 255, 0.95);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 50;
  visibility: hidden; /* 最初は隠しておく */
  opacity: 0;
}

.p-fullscreen-menu__list {
  display: flex;
  flex-direction: column;
  gap: 24px;
  text-align: center;
}

.p-fullscreen-menu__item a {
  color: #1bb4d4;
  font-size: 24px;
  font-weight: bold;
}

.l-main {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: 16px;
  height: 100svh;
  font-size: 18px;
}

【JavaScript】

// 要素の取得
const hamburger = document.querySelector('.js-hamburger');
const menu = document.querySelector('.js-menu');
const menuLinks = document.querySelectorAll('.js-menu-link');

// 1. タイムラインの初期化(最初は停止状態にしておく)
const tl = gsap.timeline({ paused: true });

// 2. タイムラインにアニメーションを登録(順番に実行される)
tl.to(menu, {
  duration: 0.4,
  autoAlpha: 1, // opacityとvisibilityを同時にコントロールする便利なプロパティ
  ease: "power2.out"
})
// メニュー背景が出た後、リンクを順番に(stagger)下からフワッと出す
.from(menuLinks, {
  duration: 0.5,
  y: 30, // 30px下から
  opacity: 0,
  stagger: 0.1, // 0.1秒ずらしながら順番に表示
  ease: "back.out(1.5)" // 少し跳ねるようなイージング
}, "-=0.2"); // 直前のアニメーションの終了0.2秒前に食い込んで開始させる

// 3. クリックイベントで再生・逆再生をコントロール
hamburger.addEventListener('click', () => {
  // ボタンのクラスを切り替えてバツ印にする
  hamburger.classList.toggle('is-active');

  // クラスが付与されていれば再生、そうでなければ逆再生
  if (hamburger.classList.contains('is-active')) {
    tl.play(); // 【処理①】アニメーションを正方向に再生
  } else {
    tl.reverse(); // 【処理②】アニメーションを逆方向に巻き戻す
  }
});

コードの解説・こだわったポイント

gsap.timeline({ paused: true }) でアニメーションを待機させる

// 1. タイムラインの初期化(最初は停止状態にしておく)
const tl = gsap.timeline({ paused: true });

今回のように「ボタンがクリックされてから」動かしたい場合は、タイムライン作成時に paused: true を指定します。これにより、ページ読み込み時に勝手にアニメーションが始まってしまうのを防ぐことができます。

.to().from() を繋げて連続処理を作る

// 2. タイムラインにアニメーションを登録(順番に実行される)
tl.to(menu, {
  ...
})
.from(menuLinks, {
  ...
}, "-=0.2"); // 直前のアニメーションの終了0.2秒前に食い込んで開始させる

タイムラインインスタンス(tl)に対して、メソッドをチェーンさせることで、順番に実行されるアニメーションを定義できます。 また、末尾に "-=0.2" のようなパラメータを渡すことで、「前の処理が終わるのを完全に待たず、少し食い気味に次の処理をスタートさせる」といった、より自然で滑らかな連携が可能です。

便利なプロパティ stagger

.from(menuLinks, {
  duration: 0.5,
  y: 30,
  opacity: 0,
  stagger: 0.1, // 0.1秒ずらしながら順番に表示
  ease: "back.out(1.5)"
}, "-=0.2");

GSAPの強力な機能のひとつが staggerです。 複数の要素(今回はメニューの各リンク js-menu-link)に対して stagger: 0.1 と指定するだけで、0.1秒ずつタイミングをずらしながら順番にアニメーションを実行してくれます。CSSの nth-child などで一つ一つ delay を計算する手間が完全に省けます!

play()reverse() で状態を管理

// クラスが付与されていれば再生、そうでなければ逆再生
if (hamburger.classList.contains('is-active')) {
  tl.play(); // 【処理①】アニメーションを正方向に再生
} else {
  tl.reverse(); // 【処理②】アニメーションを逆方向に巻き戻す
}

メニューの「開く」と「閉じる」という2つの状態を、tl.play()tl.reverse() のたった2行でコントロールしています。 GSAPが「現在どのアニメーションまで進行しているか」を記憶してくれているため、例えば「メニューが開いている途中で閉じるボタンを連打された」場合でも、アニメーションが破綻することなくスムーズに巻き戻ってくれます。

補足:公式ドキュメント

より詳しい仕様や最新の対応状況については、以下の公式ドキュメントをご参照ください。

・GSAP: TimeLine

まとめ

今回は、GSAPの timeline を使った全画面メニューの開閉アニメーションをご紹介しました。

「背景を表示させる」→「文字を順番に出す」という一連の処理が、非常にシンプルなJavaScriptコードで実装できるのがGSAPの魅力です。 stagger による順次表示はメニュー以外にも、カード一覧の表示やページ読み込み時のローディングアニメーションなど、あらゆる場面で活用できます。

これからもGSAPの機能を使いこなし、ユーザーの目を惹きつつ、コードの保守性も高いモダンな実装を心掛けていきたいと思います!

Prev
Next