JavaScript - アコーディオンメニューの作り方

アコーディオンメニューとは?

アコーディオンメニューは、選択した項目の詳細内容を開閉することができるメニューのことです。見出しとなるメニューをクリックすると、そのコンテンツが表示または非表示になります。ウェブサイトなどでよく見られますよね。

今回は、JavaScript を使って2タイプのアコーディオンメニューを作っていきます。記事の前半では、複数の項目を開くアコーディオンメニューのコードを解説し、後半からは一つの項目だけを開くコードを追加します。

この記事を読むと分かること

  • querySelectorAll()
  • forEach()
  • addEventListener()
  • classList.toggle()
  • classList.contains()
  • classList.remove()
  • nextElementSibling プロパティ
  • scrollHeight プロパティ
  • if…else 条件分岐
  • this キーワード

サンプルプロジェクト

複数のメニューを開くことができるアコーディオンメニューは、次のような動きになります。

  • メニューをクリックするとそのコンテンツが開く。右端のアイコンは「×」に変化する。
  • 再びメニューをクリックするとそのコンテンツが閉じる。右端のアイコンは「+」に変化する。
アコーディオンメニューのサンプル1

一つのメニューだけを開くアコーディオンメニューは、次のような動きになります。

  • メニューをクリックすると開く。
  • 他のメニューをクリックすると、現在開いているメニューが閉じ、クリックしたメニューが開く。
  • 選択されているメニューをクリックすると閉じる。
アコーディオンメニューのサンプル2

HTML

まずは HTML です。今回は、見出しとなるメニューを <button> 要素で三つ用意します。また、「開く・閉じる」を表すアイコンを <span> 要素で追加しています。

<h1>Accordion Menu</h1>

<div class="accordion-container">

  <!-- メニュー1 -->
  <div class="accordion">
    <!-- メニューボタン -->
    <button class="menu-button">Menu 1<span class="icon">&plus;</span></button>
    <!-- コンテンツ -->
    <div class="content">
      <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
    </div>
  </div>

  <!-- メニュー2 -->
  <div class="accordion">
    <!-- メニューボタン -->
    <button class="menu-button">Menu 2<span class="icon">&plus;</span></button>
    <!-- コンテンツ -->
    <div class="content">
      <p>Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
    </div>
  </div>

  <!-- メニュー3 -->
  <div class="accordion">
    <!-- メニューボタン -->
    <button class="menu-button">Menu 3<span class="icon">&plus;</span></button>
    <!-- コンテンツ -->
    <div class="content">
      <p>Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.</p>
    </div>
  </div>

</div>

CSS

次は CSS です。CSS でポイントとなるのは、次の2点です。

  • アイコン「+」は、メニューボタンに open クラスが追加されたときに、transform プロパティを指定して「×」へ回転するようにします。
    (open クラスの追加/削除は、この後の JavaScript で行います)
  • コンテンツは、heightoverflow プロパティを指定し、閉じた状態にします。
    (コンテンツを開くための高さ height は、この後の Javascript で取得します)
body {
  background-color: #0D7DF5;
}

h1 {
  color: #ffffff;
  text-align: center;
  margin-bottom: 30px;
}

.accordion-container {
  width: 90%;
  max-width: 500px;
  margin: 0 auto;
}

.menu-button {
  width: 100%;
  padding: 10px 20px;
  margin-top: 2px; 
  font-size: 18px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  border: none;
  background-color: #e6e6e6;
  cursor: pointer;
}

.menu-button:hover {
  background-color: #ffffff;
}

.icon {
  font-size: 20px;
  transition: 0.4s;
}

/* メニューボタンにopenクラスがついているときの
 アイコンのスタイル「×」 */
.menu-button.open .icon {
  transform: rotate(45deg);
}

.content {
  padding: 0 20px;
  background-color: #f2f2f2;
  transition: 0.4s;
  /* コンテンツを閉じる */
  height: 0;
  overflow: hidden;
}
アコーディオンメニュー HTML + CSS

JavaScript

ではここから、JavaScript を書いていきましょう。最初に、「複数のメニューを開くタイプ」のアコーディオンメニューを解説します。後ほど解説する「一つのメニューだけを開くタイプ」も、このコードがベースになりますよ。

すべてのメニューボタンにクリックイベントを追加する

JavaScript で最初にすることは、メニューボタンをクリックできるようにすることです。そのためのコードを書いていきましょう。

  • すべてのメニューボタンを querySelectorAll() メソッドでまとめて取得し、変数 menuBtns に保存します。
// すべてのメニューボタンを取得
const menuBtns = document.querySelectorAll('.menu-button');
  • すべてのメニューボタンをクリックできるようにするために、forEach() メソッドでメニューボタン一つひとつに処理を行います。
const menuBtns = document.querySelectorAll('.menu-button');

// メニューボタン一つひとつに処理を行う
menuBtns.forEach(menuBtn => {

  // メニューボタンにクリックイベントを追加
  // …

});
  • addEventListener() メソッドで、メニューボタンがクリックされたときに関数が実行されるようにします。
const menuBtns = document.querySelectorAll('.menu-button');

menuBtns.forEach(menuBtn => {

  // メニューボタンにクリックイベントを追加
  menuBtn.addEventListener('click', function() {

    // クリックされたときに実行する処理
    // …

  });
});

クリックされたときに実行する関数は、アロー関数ではなく function キーワードを使って書いてください。その理由は、後ほど説明します。

アイコンの見た目を変化させる

メニューボタンをクリックできるようになったら、クリックされたときに実行する関数を書いていきましょう。

まずは、アイコンの見た目を切り替えます。アイコンを「+」と「×」で変化させるためには、classList.toggle() メソッドで open クラスの追加/削除を行います。

  • クリックされたメニューボタンに、 open クラスの付け外しをします。
const menuBtns = document.querySelectorAll('.menu-button');

menuBtns.forEach(menuBtn => {
  menuBtn.addEventListener('click', function() {

    // openクラスの追加または削除
    this.classList.toggle('open');

  });
});

イベントハンドラー内で使われている this は、イベントが発生して関数を実行している要素(クリックされたメニューボタン)を参照します。クリックされたときに実行する関数を function キーワードを使って書くのは、この this を有効にするためです。(アロー関数では、この this は正しく動きません)

メニューボタンをクリックすると open クラスの追加/削除が切り替わるので、アイコンの見た目もクリックのたびに「+」と「×」で変化するようになりました。

コンテンツを開閉する

次は、クリックされたメニューのコンテンツを開閉できるようにします。該当のコンテンツを取得するために使うのは、次の兄弟要素を指す nextElementSibling プロパティです。

兄弟要素とは、同じ親を持つ要素のことです。メニューボタンと同じ親を持ち、メニューボタン次にある要素は、そのコンテンツですね。

親と子、兄弟要素の関係

メニュー1がクリックされたらメニュー1のコンテンツを、メニュー2がクリックされたらメニュー2のコンテンツを取得できるようにしましょう。

  • クリックされたメニューボタン this の次の兄弟要素を取得して、変数 content に格納します。
const menuBtns = document.querySelectorAll('.menu-button');

menuBtns.forEach(menuBtn => {
  menuBtn.addEventListener('click', function() {
    this.classList.toggle('open');

    // クリックされたメニューボタンのコンテンツを取得
    const content = this.nextElementSibling;

  });
});

続いて、コンテンツを開くのか閉じるのか、 if…else で処理を分けます。classList.contains() メソッドを使い、メニューボタンに open クラスが含まれているかどうかを調べましょう。

  • もし、クリックされたメニューボタンに open クラスが含まれているなら
const menuBtns = document.querySelectorAll('.menu-button');

menuBtns.forEach(menuBtn => {
  menuBtn.addEventListener('click', function() {
    this.classList.toggle('open');
    const content = this.nextElementSibling;

   // openクラスが含まれているかどうかによってコンテンツを開くまたは閉じる
    if (this.classList.contains('open')) {
      // 開く
    } else {
      // 閉じる
    }
  });
});

条件を満たす場合は、コンテンツを開きます。コンテンツは、CSS でheight: 0 にしましたね。高さが 0 のままだと表示されないので、scrollHeight プロパティでコンテンツの高さを取得し、その値をコンテンツの高さ height とします。

  • コンテンツの高さを取得して、コンテンツを開きます。
const menuBtns = document.querySelectorAll('.menu-button');

menuBtns.forEach(menuBtn => {
  menuBtn.addEventListener('click', function() {
    this.classList.toggle('open');
    const content = this.nextElementSibling;
    if (this.classList.contains('open')) {

      // コンテンツを開く
      content.style.height = content.scrollHeight + 'px';

    } else {
      // 閉じる
    }
  });
});

クリックされたメニューボタンに open クラスが含まれていない場合は、コンテンツを閉じましょう。

  • コンテンツの高さを 0 にして、コンテンツを閉じます。
const menuBtns = document.querySelectorAll('.menu-button');

menuBtns.forEach(menuBtn => {
  menuBtn.addEventListener('click', function() {
    this.classList.toggle('open');
    const content = this.nextElementSibling;
    if (this.classList.contains('open')) {
      content.style.height = content.scrollHeight + 'px';
    } else {

      // コンテンツを閉じる
      content.style.height = 0;
      
    }
  });
});

以上で、複数のメニューを開くタイプのアコーディオンメニューのコードが完成です。

他のメニューが開いていたら閉じる

ここからは、一つのメニューだけを開くためのコードを追加していきましょう。クリックしたメニュー以外の項目が開いている場合は、自動的に閉じるようにしますよ。

const menuBtns = document.querySelectorAll('.menu-button');

menuBtns.forEach(menuBtn => {
  menuBtn.addEventListener('click', function() {


    // 一つのメニューだけを開くためのコードをここに追加
    // …


    //-----------------------------------------------
    this.classList.toggle('open');
    const content = this.nextElementSibling;
    if (this.classList.contains('open')) {
      content.style.height = content.scrollHeight + 'px';
    } else {
      content.style.height = 0;
    }
  });
});

先に解説したコードでは、「クリックされたメニューボタン」にだけ注目して処理を行いました。これから追加するコードで注目するのは、「開いているメニュー」です。メニューボタンに open クラスが付いているのが、開いているメニューですね。

  • 開いているメニューを取得して、変数 activeAccordion に格納します。
const menuBtns = document.querySelectorAll('.menu-button');

menuBtns.forEach(menuBtn => {
  menuBtn.addEventListener('click', function() {

    // 開いているメニューを取得
    const activeAccordion = document.querySelector('.menu-button.open');


    //-----------------------------------------------
    this.classList.toggle('open');
    const content = this.nextElementSibling;
    if (this.classList.contains('open')) {
      content.style.height = content.scrollHeight + 'px';
    } else {
      content.style.height = 0;
    }

  });
});

場合によっては、メニューがすべて閉じていることもあるため、この後の処理には if 文で条件をつけますよ。クリックしたメニュー以外に開いている項目がある場合だけ、それを閉じる処理を実行します。

  • 開いているメニューがあって、かつ、それがクリックしたメニューではないなら
const menuBtns = document.querySelectorAll('.menu-button');

menuBtns.forEach(menuBtn => {
  menuBtn.addEventListener('click', function() {

    const activeAccordion = document.querySelector('.menu-button.open');

    // クリックしたメニュー以外に開いている項目があるなら
    if(activeAccordion && activeAccordion !== this) {
      
      // 閉じる
      // …
      
    }

    //-----------------------------------------------
    this.classList.toggle('open');
    const content = this.nextElementSibling;
    if (this.classList.contains('open')) {
      content.style.height = content.scrollHeight + 'px';
    } else {
      content.style.height = 0;
    }

  });
});
  • nextElementSibling プロパティで開いているメニューのコンテンツを取得し、その高さを 0 にして閉じます。
  • classList.remove() メソッドで open クラスを削除し、アイコンを「+」に戻します。
const menuBtns = document.querySelectorAll('.menu-button');

menuBtns.forEach(menuBtn => {
  menuBtn.addEventListener('click', function() {

    const activeAccordion = document.querySelector('.menu-button.open');
    if(activeAccordion && activeAccordion !== this) {

      // 開いているメニューを閉じる
      activeAccordion.nextElementSibling.style.height = 0;
      // アイコンを+にする
      activeAccordion.classList.remove('open');

    }

    //-----------------------------------------------
    this.classList.toggle('open');
    const content = this.nextElementSibling;
    if (this.classList.contains('open')) {
      content.style.height = content.scrollHeight + 'px';
    } else {
      content.style.height = 0;
    }

  });
});

以上で、一つのメニューだけを開くためのコードを追加できました。

完成コード

2タイプのアコーディオンメニューのコードが完成です。

//*****************************
// アコーディオンメニューのコード
//*****************************

const menuBtns = document.querySelectorAll('.menu-button');

menuBtns.forEach(menuBtn => {
  menuBtn.addEventListener('click', function() {

    //----- 一つだけ開く場合に追加するコード --------------
    const activeAccordion = document.querySelector('.menu-button.open');
    // クリックしたメニュー以外に開いている項目があるなら閉じる
    if(activeAccordion && activeAccordion !== this) {
      activeAccordion.nextElementSibling.style.height = 0;
      activeAccordion.classList.remove('open');
    }
    //------------------------------------------------

    // クリックしたメニューを開くまたは閉じる
    this.classList.toggle('open');
    const content = this.nextElementSibling;
    if (this.classList.contains('open')) {
      content.style.height = content.scrollHeight + 'px';
    } else {
      content.style.height = 0;
    }
    
  });
});

See the Pen JavaScript - Accordion Menu by Pyxofy (@pyxofy) on CodePen.

Pyxofy (著)「きょうからはじめるスクラッチプログラミング入門」

Pyxofy が Scratch の電子書籍を出版しました!Kindle・Apple Books からご購入ください。

詳細はこちら

まとめ

今回は、JavaScriptでアコーディオンメニューを作成しました。

メニューを開いたり閉じたりするために行ったのは、クラスの付け外しです。それによって、「開く・閉じる」のアイコンの見た目を変化させたり、コンテンツの表示・非表示を切り替えました。

最後まで読んでいただき、ありがとうございます。この記事をシェアしてくれると嬉しいです!


SNSで Pyxofy とつながりましょう! LinkedInThreadsMastodon X (Twitter) @pyxofyFacebook

関連記事

JavaScript - タブメニューの作り方
ウェブページに表示する内容を簡単に切り替えることができるタブメニューを作ります。JavaScript の forEach() メソッドと for...of ループの反復処理でクラスの付け外しを行う方法を学びましょう。
CSS Art
Articles for creating CSS Art.
CSS Animation
Articles for creating CSS Animation.
Scratch - Pyxofy
Scratch 3.0の使い方を、プログラミング初心者や子どもにも分かりやすく紹介しています。