CONTACT

お問い合わせ

GSAPでシンプルにアコーディオン実装のメモ

HOME ブログ GSAPでシンプルにアコーディオン実装のメモ

はじめに

WebサイトのUIでよく見かけるアコーディオンを、アニメーションライブラリのGSAPで実装するメモです。GSAP(GreenSock Animation Platform)は、JavaScriptでリッチなアニメーションを簡単・高性能に実装できるライブラリです。
※本メモではアコーディオンの動作に必要なコードのみ記載していますので、全てのコードはcodepenからご確認ください。

DEMO

See the Pen accordion by KAZUKI YOKOUCHI (@tone-design) on CodePen.

HTML

<div class="accordion js-accordion" data-accordion-state="close">
  <div class="accordion__header js-accordion-opener">
    <p>アコーディオンを開く</p>
  </div>
  <div class="accordion__content js-accordion-content">
    <p>アコーディオンの内容がはいります。アコーディオンの内容がはいります。アコーディオンの内容がはいります。アコーディオンの内容がはいります。</p>
  </div>
</div>

アコーディオン全体のクラス名をjs-accordion、クリックで開閉する部分を js-accordion-opener、実際に開閉されるコンテンツを js-accordion-content としています。
※CSS用のクラスと区別するため、JavaScriptで操作する要素は接頭辞を js-としています。

data-accordion-state はアコーディオンの開閉状態を管理するためのカスタムデータ属性です。
CSS の属性セレクタを用いて、開閉時のCSSによるスタイル分けのため利用します。
今回はプラス/マイナスのアイコンを切り替えをCSSで設定しています。
閉じているときは close、開いているときは openとし、JavaScriptで切り替えます。
初期値の状態は閉じているのでcloseです。

CSS

.accordion__content {
  display: none;
}

CSS は .accordion__content に初期値として display: none; を設定し、アコーディオンの開閉コンテンツを非表示にするのみです。

[data-accordion-state='close'] .accordion__header:after {
  /*閉じている時(プラスアイコン) */
  content: '\2b';
}

[data-accordion-state='open'] .accordion__header:after {
  /*開いている時(マイナスアイコン) */
  content: '\f068';
}
// アコーディオンの要素を全て取得
const accordions = document.querySelectorAll('.js-accordion');

// アニメーション中かどうかを管理するフラグ
// weakmapを使うことで、DOM要素に直接プロパティを追加せずに状態を管理できる
const animatingFlag = new WeakMap();

accordions.forEach((accordion) => {
  // 各アコーディオン内の開閉要素とコンテンツを取得
  const opener = accordion.querySelector('.js-accordion-opener');
  const content = accordion.querySelector('.js-accordion-content');

  // 初期化 / 全てのアコーディオンは「アニメーションしていない」状態からスタート
  animatingFlag.set(accordion, false);

  opener.addEventListener('click', () => {
    // すでにアニメーション中なら処理をしない
    if (animatingFlag.get(accordion)) return;

    // アニメーション中フラグをセット
    animatingFlag.set(accordion, true);

    // 進行中のGSAPアニメーションを強制終了して競合を防ぐ
    gsap.killTweensOf(content);

    // 開閉状態を取得(true:open, false:close)
    const isOpen = accordion.getAttribute('data-accordion-state') === 'open';

    if (!isOpen) {
      // ▼ 開く処理(closeの状態) ▼
      // openへ状態をセット
      accordion.setAttribute('data-accordion-state', 'open');

      // 要素を表示して高さ0にセット
      gsap.set(content, {
        display: 'block',
        height: 0,
      });

      // scrollHeightまでアニメーションで展開
      gsap.to(content, {
        height: content.scrollHeight,
        duration: 0.3,
        onComplete: () => {
          // 開いた後は高さをautoに戻してレスポンシブに対応
          accordion.setAttribute('data-accordion-state', 'open');
          gsap.set(content, {
            height: 'auto',
          });
          // アニメーション中フラグを解除
          animatingFlag.set(accordion, false);
        },
      });
    } else {
      // ▼ 閉じる処理(openの状態) ▼
      // closeへ状態をセット
      accordion.setAttribute('data-accordion-state', 'close');

      // 高さ0にして、アニメーション後に非表示にする
      gsap.to(content, {
        height: 0,
        duration: 0.3,
        onComplete: () => {
          gsap.set(content, { display: 'none' });
          // アニメーション中フラグを解除
          animatingFlag.set(accordion, false);
        },
      });
    }
  });
});

開閉アニメーション中かどうかの管理

const animatingFlag = new WeakMap();

アコーディオンの開閉アニメーション中の開閉連打による意図しない動作を防ぐため、今回はJavaScriptの組み込みオブジェクトのWeakMapによる「開閉アニメーション中かどうか」の状態を管理・チェックします。

accordions.forEach((accordion) => {
  // 各アコーディオン内の開閉要素とコンテンツを取得
  const opener = accordion.querySelector('.js-accordion-opener');
  const content = accordion.querySelector('.js-accordion-content');

  // 初期化 / 全てのアコーディオンは「アニメーションしていない」状態からスタート
  animatingFlag.set(accordion, false);

初期化として、全てのアコーディオン要素に「アニメーションしていない状態」としてfalseをセットします。

opener.addEventListener('click', () => {
    // すでにアニメーション中なら処理をしない
    if (animatingFlag.get(accordion)) return;

    // アニメーション中フラグをセット
    animatingFlag.set(accordion, true);

各アコーディオン要素の開閉ボタンをクリックしたとき、animatingFlagtrueであれば「すでにアニメーション中」と判断して処理を中断し、連打による意図しない動作を防ぎます。
falseの場合はanimatingFlagtrueをセットし、「アニメーション中の状態」としています。

 gsap.set(content, {
  display: 'block',
  height: 0,
});

// scrollHeightまでアニメーションで展開
gsap.to(content, {
  height: content.scrollHeight,
  duration: 0.3,
  onComplete: () => {
    // 開いた後は高さをautoに戻してレスポンシブに対応
    accordion.setAttribute('data-accordion-state', 'open');
    gsap.set(content, {
      height: 'auto',
    });
    // アニメーション中フラグを解除
    animatingFlag.set(accordion, false);
  },
});

開く処理

content.scrollHeight は、要素の「中身の高さ(スクロール可能な実際の高さ)」を取得するプロパティです。
CSSで height: auto; を指定したときと同じような「コンテンツの実寸の高さ」を数値(px)で返します。

アコーディオンの開閉アニメーションでは、CSSで .accordion__contentdisplay: none; にして非表示にしているので、開くときにはまず gsap.set() を使って要素を display: block に変更し、さらに height: 0px を指定して、高さが0の状態からアニメーションを開始します。

その後、gsap.to() を使って content.scrollHeight の値(要素の中身の実際の高さ)までアニメーションさせることで、アコーディオンをスムーズに開きます。
※アニメーションが完了したら、animatingFlagfalse にセットし「アニメーションしていない状態」とします。

閉じる処理

gsap.to(content, {
  height: 0,
  duration: 0.3,
  onComplete: () => {
    gsap.set(content, { display: 'none' });
    // アニメーション中フラグを解除
    animatingFlag.set(accordion, false);
  },
});

閉じるときは、gsap.to()height: 0 を指定し、アコーディオンのコンテンツを縮める(0px)にするアニメーションを実行します。これで開閉の一連の動作が完了です。
※アニメーションが完了したら、animatingFlagfalse にセットし「アニメーションしていない状態」とします。

おわりに

アコーディオン単体で GSAP を導入することは少ないかと思いますが、サイト全体で GSAP を利用している場合にはシンプルに実装でき、便利です。

HOME ブログ GSAPでシンプルにアコーディオン実装のメモ