• 作成日:

CSSのみでアコーディオンメニューを実装する - 実務で使えるのか検証する

CSSのみでアコーディオンメニューを実装する - 実務で使えるのか検証する

今回はJavaScriptを使用せず、アコーディオンメニューをHTMLとCSSだけで実装する方法と実務でも使えるのか検証します。

前提条件は以下

  • HTMLとCSSのみで実装
  • 開閉のときにアニメーションさせる

説明環境は以下

  • macOS Monterey 12.5.1
  • Visual Studio Code v1.73.1
この記事の目次

CSSのみでアコーディオンメニューを実装する方法

アコーディオンメニューはJavaScriptを使用しなくてもCSSのみで実装できます。ただし、メリット デメリットがあるので正しく理解して実装しましょう。後ほど説明します。

HTMLで書くこと

HTMLにはdetails要素summary要素を使ってアコーディオンメニューを実装する方法がありますが、CSSのみだと閉まるときにアニメーションさせることができないので、今回はinput要素とlabel要素を使っています。

<div class="p-accordion p-accordion__input">
 <div class="p-accordion-desc">●HTMLとCSSのみ</div>
   <input id="p-accordion__01" type="checkbox" name="p-accordion__block">
   <label class="p-accordion__head" for="p-accordion__01"><span class="p-accordion__head-inner">Human<span class="p-accordion__icon"></span></span></label>
   <div class="p-accordion__content">
     <div class="p-accordion__main">A place where people live. The world. The world. The world in which people live and relate to each other. It is also a word that conceptually expresses the fragile and ephemeral state of such human society.</div>
   </div>
   <input id="p-accordion__02" type="checkbox" name="p-accordion__block">
   <label class="p-accordion__head" for="p-accordion__02"><span class="p-accordion__head-inner">Gorilla<span class="p-accordion__icon"></span></span></label>
   <div class="p-accordion__content">
     <div class="p-accordion__main">An ape living in Africa. They are very large and strong. They live in small families in tree nests.</div>
   </div>
   <input id="p-accordion__03" type="checkbox" name="p-accordion__block">
   <label class="p-accordion__head" for="p-accordion__03"><span class="p-accordion__head-inner">Chimpanzee<span class="p-accordion__icon"></span></span></label>
   <div class="p-accordion__content">
     <div class="p-accordion__main">An ape living in the forests of western and central Africa. They are about 150 cm long and black all over. They form groups, mainly consisting of males. They are cheerful, vocal, and intelligent.</div>
   </div>
</div>

ポイントは以下です。

  • input要素のtypeはcheckboxにする
  • input要素のid名とlabel要素のfor属性を同じ名前にすること
  • input要素、label要素、コンテンツ部分のHTML階層を同じにする

CSSで書くこと

.p-accordion {
  max-width: 700px;
  margin-inline: auto;
}
.p-accordion-desc {
  font-size: 15px;
  font-weight: 700;
}
.p-accordion__head {
  cursor: pointer;
  background-color: #26a69a;
  display: block;
  color: white;
  padding: 7px 20px;
  margin-top: 10px;
}
.p-accordion__head-inner {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.p-accordion__icon {
  display: block;
  position: relative;
  width: 24px;
  transition: transform 0.25s;
}
.p-accordion__icon:before {
  content: "";
  position: absolute;
  display: block;
  width: 15px;
  height: 2px;
  background-color: #fff;
}
.p-accordion__icon:after {
  content: "";
  position: absolute;
  display: block;
  width: 15px;
  height: 2px;
  background-color: #fff;
}
.p-accordion__icon:before {
  left: 0;
  transform: rotate(45deg);
}
.p-accordion__icon:after {
  right: 0;
  transform: rotate(-45deg);
}
.p-accordion__input input[name=p-accordion__block] {
  display: none;
}
.p-accordion__input .p-accordion__block {
  cursor: pointer;
}
.p-accordion__input .p-accordion__content {
  height: 0;
  padding: 0 20px;
  overflow: hidden;
  opacity: 0.5;
  background-color: #fff;
  transition: padding 0.25s ease, opacity 0.3s ease 0s;
}
.p-accordion__input input[name=p-accordion__block]:checked + .p-accordion__head > .p-accordion__head-inner .p-accordion__icon {
  transform: rotate(-180deg);
}
.p-accordion__input input[name=p-accordion__block]:checked + .p-accordion__head + .p-accordion__content {
  /*開閉時*/
  height: auto;
  opacity: 1;
  padding: 20px;
}

input要素のチェック機能だけ利用するので、input自体は以下のように非表示にしています。

.p-accordion__input input[name=p-accordion__block] {
  display: none;
}

アコーディオンメニューのコンテンツ部分は、以下のようにheight:0で高さを消し、overflow:hiddenで見えないようにしています。opacityはチラつきを緩和するために指定しています。

.p-accordion__input .p-accordion__content {
  height: 0;
  padding: 0 20px;
  overflow: hidden;
  opacity: 0.5;
  background-color: #fff;
  transition: padding 0.25s ease, opacity 0.3s ease 0s;
}

input要素は非表示にしていますが、check機能は生きているので、チェックが入ればCSS側のcheckedで判定できます。これを利用してチェックが入ったらアコーディオンメニューを開閉させるようにしています。

.p-accordion__input input[name=p-accordion__block]:checked + .p-accordion__head + .p-accordion__content {
  /*開閉時*/
  height: auto;
  opacity: 1;
  padding: 20px;
}

開閉時のアニメーションについては、height:0からheight:autoへ変化ではアニメーションされません。そこでpaddingとopacityの変化に対してアニメーションさせています。

実装する場合の注意点

CSSのみで手軽に実装できますが、デメリットは以下です。
このことからHTMLとCSSだけで動きのあるアコーディオンメニューを実務で実装するのは難しいかもしれません。

  • キーボードによるフォーカスや開閉ができない
  • Command + F(WindowsはControl + F)による単語検索でヒットしても、閉じているコンテンツ部分が開かない
  • アニメーションがheightの変化で発火できない

JavaScriptを使ってアコーディオンメニューを実装する

アニメーション機能を含めたアコーディオンメニューを実装するにはJavaScriptを使う必要があります。先ほどのinputバージョンを含め、全4パターンのデモページを作成したので動きを比較してみてください。

  • HTMLとCSSのみ
  • HTMLとCSS + JavaScript(setTimeoutとclearTimeout)
  • HTMLとCSS + JavaScript( Web Animations API )
  • HTMLとCSS + JavaScript(GSAP)

尚、コードの一部についてはICS MEDIAさんの記事を参考にしています。

HTMLで書くこと(JavaScriptバージョン)

JavaScriptを使う場合はHTML構造をdetails要素とsummary要素を使い、すべて同じにしています。

<div class="p-accordion">
    <details class="p-accordion__block js-accordion">
       <summary class="p-accordion__head js-accordion-head">
          <span class="p-accordion__head-inner">Human<span class="p-accordion__icon"></span></span>
       </summary>
       <div class="p-accordion__content js-accordion-main">
          <div class="p-accordion__main">A place where people live. The world. The world. The world in which people live and relate to each other. It is also a word that conceptually expresses the fragile and ephemeral state of such human society.</div>
       </div>
    </details>
    <details class="p-accordion__block js-accordion">
       <summary class="p-accordion__head js-accordion-head">
          <span class="p-accordion__head-inner">Gorilla<span class="p-accordion__icon"></span></span>
       </summary>
       <div class="p-accordion__content js-accordion-main">
          <div class="p-accordion__main">An ape living in Africa. They are very large and strong. They live in small families in tree nests.</div>
       </div>
    </details>
    <details class="p-accordion__block js-accordion">
       <summary class="p-accordion__head js-accordion-head">
          <span class="p-accordion__head-inner">Chimpanzee<span class="p-accordion__icon"></span></span>
       </summary>
       <div class="p-accordion__content js-accordion-main">
          <div class="p-accordion__main">An ape living in the forests of western and central Africa. They are about 150 cm long and black all over. They form groups, mainly consisting of males. They are cheerful, vocal, and intelligent.</div>
       </div>
    </details>
</div>

summary要素をクリックすると、details要素にopen属性がつき、それがトリガーでコンテンツ部分が開く。これがsummaryとdetails要素の簡単な説明です。

詳しくは以下をどうぞ。

JavaScriptに書くこと(setTimeoutとclearTimeout)

まずはsetTimeoutとclearTimeoutを使ってアコーディオンメニューを実装してみます。
(CSSについてはデモページで確認してください。)

const Accordion = () => {
  const accordionBody = document.querySelectorAll('.js-accordion');
  const IS_OPENED_CLASS = 'is-opened'; // アイコン操作用のクラス名
  accordionBody.forEach((elem) => {
    const head = elem.querySelector('.js-accordion-head');
    const content = elem.querySelector('.js-accordion-main');
    if (!(head && content)) return;
    let timeoutId;
    const msM = 250;
    content.style.setProperty('transition', `height ${msM}ms ease`);

    head.addEventListener('click', (event) => {
      // デフォルトの挙動を無効化
      event.preventDefault();

      // 閉じる
      if (elem.hasAttribute('open')) {
        clearTimeout(timeoutId);
        // トリガーの'is-opened'クラスを削除
        elem.classList.remove(IS_OPENED_CLASS);
        content.style.height = '';
        timeoutId = setTimeout(() => {
          elem.removeAttribute('open');
        }, msM);
      // 開く
      } else {
        elem.classList.add(IS_OPENED_CLASS);
        elem.setAttribute('open', 'true');
        content.style.height = `${content.scrollHeight}px`;
      }
    });
  });
};
Accordion();

summary要素はクリックした瞬間に、コンテンツ部分が開閉してしまい、アニメーションができないので、以下でデフォルトの機能を無効化しています。

// デフォルトの挙動を無効化
event.preventDefault();

一見、機能しているように見えますが、連続してクリックすると開閉する機能はclearTimeoutによって止まってしまいます。またCSS側でコンテンツ部分のheightを0にしているため、サイト内検索で単語がヒットしても自動で開閉されず、アクセシビリティ面でもよくありません。

setTimeoutとclearTimeoutは古くからある書き方で楽ですが、十分とは言えないでしょう。

JavaScriptに書くこと(Web Animations API)

Web Animations APIとは、keyframeを使ったアニメーションがJavaScript上で実装できるというもの。

const AccordionAnimation = () => {
  const accordionBody = document.querySelectorAll('.js-accordion');
  const RUNNING_VALUE = 'running';
  const IS_OPENED_CLASS = 'is-opened'; // アイコン操作用のクラス名
  accordionBody.forEach((elem) => {
    const head = elem.querySelector('.js-accordion-head');
    const content = elem.querySelector('.js-accordion-main');
    if (!(head && content)) return;

    head.addEventListener('click', (event) => {
      // デフォルトの挙動を無効化
      event.preventDefault();
      // データ属性に"running"があれば処理終了
      if (elem.dataset.animStatus === RUNNING_VALUE) return;
      
      const animTiming = {
        duration: 250,
        easing: 'ease',
      };
      /**
       * 閉じるときのキーフレーム
       */
      const closingAnimKeyframes = (_content) => [
        {
          height: `${_content.scrollHeight}px`, 
          opacity: 1,
        },
        {
          height: '0',
          opacity: 0,
        },
      ];
      /**
       * 開くときのキーフレーム
       */
      const openingAnimKeyframes = (_content) => [
        {
          height: '0',
          opacity: 0,
        },
        {
          height: `${_content.scrollHeight}px`,
          opacity: 1,
        },
      ];
      if (elem.hasAttribute('open')) {
        const closingAnim = content.animate(closingAnimKeyframes(content), animTiming);
        // アニメーション実行中はdata属性に"running"をつける
        elem.dataset.animStatus = RUNNING_VALUE;
        elem.classList.toggle(IS_OPENED_CLASS);
        closingAnim.onfinish = () => {
          // アニメーションの完了後にopen属性を取り除く
          elem.removeAttribute('open');
          elem.dataset.animStatus = '';
        };
      } else {
        elem.setAttribute('open', 'true');
        const openingAnim = content.animate(openingAnimKeyframes(content), animTiming);
        elem.dataset.animStatus = RUNNING_VALUE;
        elem.classList.toggle(IS_OPENED_CLASS);
        openingAnim.onfinish = () => {
          elem.dataset.animStatus = '';
        };
      }
    });
  });
};

AccordionAnimation();

Web Animation APIにはアニメーションが終了したあとに発火するメソッドが用意されています。onfinishを使ってアニメーションが終わりを検知してから、details要素のopen属性を操作しています。

また、アニメーションが実行中にはdata属性にrunningをつけて、このrunningがdetails要素に残っているときはクリックされても処理されません。これで連続してクリックされてもある程度対応できます。

JavaScriptに書くこと(GSAP)

GSAPとはスムーズなアニメーションを実現できるライブラリです。これを使うことでアコーディオンメニューの動きが実現できます。

まずはGSAPを読み込みます。
今回はCDNで読み込んでみました。

<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.11.4/gsap.min.js"></script>

そして以下のように書きます。

const AccordionGSAP = () => {
  const accordionBody = document.querySelectorAll('.js-accordion');
  accordionBody.forEach((elem) => {
    const head = elem.querySelector('.js-accordion-head');
    const content = elem.querySelector('.js-accordion-main');
    const IS_OPENED_CLASS = 'is-opened'; // アイコン操作用のクラス名
    if (!(head && content)) return;
   
    const closingAnim = (_content, _elem) =>
       /**
       * 閉じるとき
       */
      gsap.to(_content, {
        height: '0',
        opacity: 0,
        duration: 0.25,
        ease: 'ease',
        overwrite: true, //アニメーションの上書きを有効
        onComplete: () => {
          _elem.removeAttribute('open');
        },
      });

    const openingAnim = (_content) =>
       /**
       * 開くとき
       */
      gsap.fromTo(
        _content,
        {
          height: '0',
          opacity: 0,
        },
        {
          height: 'auto',
          opacity: 1,
          duration: 0.25,
          ease: 'ease',
          overwrite: true, //アニメーションの上書きを有効
        }
      );

    head.addEventListener('click', (event) => {
      event.preventDefault();
      if (elem.classList.contains(IS_OPENED_CLASS)) {
        elem.classList.toggle(IS_OPENED_CLASS);
        closingAnim(content, elem).restart();
      } else {
        elem.classList.toggle(IS_OPENED_CLASS);
        elem.setAttribute('open', 'true');
        openingAnim(content).restart();
      }
    });
  });
};

AccordionGSAP();

開く時は特定の状態から特定の状態へアニメーションさせるgsap.fromToを使い、閉じるときは今ある状態から特定の状態へアニメーションさせるgsap.Toを使います。

閉じるときのgsap.Toでは、アニメーションが完了したときに発火するonCompleteメソッドが用意されているので、これを使ってdetailsにあるopen属性を削除しています。

また、overwrite:trueにすることでアニメーションが走っていても、別のアニメーションが実行されれば、強制終了して上書きされます。これによって連続してクリックされても動きのガタつきは無くなります。

開閉の判定はdetails要素にopen属性があるかどうかではなく、is-opendクラスがあるかないかです。gsapによってアニメーションからopen属性の操作まで、一連の流れになるのでopen属性で判定してしまうと正しく機能しません。

GSAPについては公式をどうぞ。

結局、どれを使えば良いのか?

結論としては以下の2つどちらかを使えば間違いないでしょう。

  • HTMLとCSS + JavaScript( Web Animations API )
  • HTMLとCSS + JavaScript(GSAP)

GSAPをサイトのどこかで使っているのであれば、アコーディオンメニューにも実装しても良いかもしれません。そうでなければ、Web Animations APIで十分です。

もう1度アコーディオンメニューの比較デモを確認してみて判断してみてください。

さいごに

今回はJavaScriptを使用せず、アコーディオンメニューをHTMLとCSSだけで実装する方法を紹介しました。結果的にアコーディオンメニューにはdetails要素とsummary要素を使う必要があること。それから開閉まで、なめらかなアニメーションをさせるにはJavaScriptが必要だということがわかりました。

CSSのみで実装できることも多くなってきましたが、まだまだ細かいところはJavaScriptと併用して実装する必要がありそうです。