JavaScriptで簡単なアコーディオンメニューを作る方法

テーマ

こんにちは!!naoto555です。
今回は、JavaScriptで簡単なアコーディオンメニューを作成する方法について解説していきます。アコーディオンメニューとは、ボタンをクリックするとメニューが開閉するUIのことです。この記事では、基本的なアコーディオンメニューの実装方法から、応用的な使い方まで、幅広く解説していきます。JavaScriptの基礎知識がある方であれば、誰でも簡単に実装できる内容になっています。是非、最後までご覧ください!(2024.2記載)

基本的な実装

まずはボタンを押すとメニューが開閉するような基本的な実装例を以下に紹介します。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .accordion-container {
            width: 50%;
            margin: 50px auto;
        }
        .accordion-header {
            cursor: pointer;
            background-color: #eee;
            padding: 5px 10px;
            border: none;
            border-bottom: solid 1px #ccc;
            outline: none;
        }
        .accordion-header.active {
            background-color: #ccc;
        }
        .accordion-content {
            display: none;
            overflow: hidden;
            height: 50px;
            padding: 10px;
            border: solid 1px #ccc;
            border-top: none;
        }
    </style>
</head>
<body>
    <div class="accordion-container">
        <div class="accordion">
            <div class="accordion-header">タイトル1</div>
            <div class="accordion-content">内容1</div>
        </div>
        <div class="accordion">
            <div class="accordion-header">タイトル2</div>
            <div class="accordion-content">内容2</div>
        </div>
        <div class="accordion">
            <div class="accordion-header">タイトル3</div>
            <div class="accordion-content">内容3</div>
        </div>
    </div>


    <script type="text/javascript">
        const accordionHeaders = document.querySelectorAll('.accordion-header');
        accordionHeaders.forEach((accordionHeader) => {
            accordionHeader.addEventListener('click', () => {
                accordionHeader.classList.toggle('active');
                const accordionContent = accordionHeader.nextElementSibling;

                if (accordionContent.style.display === 'block') {
                    accordionContent.style.display = 'none';
                } else {
                    accordionContent.style.display = 'block';
                }
            });
        });
    </script>
</body>
</html>

タイトル(.accordion-header)をクリックすると、タイトルに対応するコンテンツ(.accordion-content)が表示されるものとなっています。

“elements.forEach(element) => { //処理  }”と書くことで、配列elementsの各要素が順にelementに代入されることになります。.forEach()メソッドでは、コールバック関数に配列やオブジェクトの要素が順番に渡されるため、処理を繰り返すことができます。

“classList.toggle(‘active’)”は、対象の要素のクラス名に’active’というクラス名が設定されていれば削除し、なければ追加するというメソッドになります。
toggleの文字通り、”accordion-header”と”accordion-header active”というようにクラス名を要素のクリックによってトグル(切替え)しています。

“要素.nextElementSibling”では、要素と同じ階層にある次の要素を選択するメソッドです。これにより<div class="accordion-header">の次の要素つまり<div class="accordion-content">が選ばれ、タイトル(.accordion-header)をクリックされたときに表示・非表示を切り替えています。

以下は動作させている動画です。

 

応用したコード①

では、応用例として先ほどの基本コードのスタイルタグの中身とJavaScriptに少し変更を加え、簡単なアニメーションを作ってみましょう。
まずは、スタイルタグの中身に以下のような変更を加えます。

.accordion-content {
    /*display: none;*/            
    overflow: hidden;
    height: 0;         /* 50px => 0 */
    padding: 10px;
    border: solid 1px #ccc;
    border-top: none;
    /*以下のプロパティを追加*/
    transition: hight 1s ease-in, background-color 3s ease-in-out;
}

/* 以下、要素を追加 */
.accordion-content.active {
    height: auto;
    background-color: lightblue;
}

次にJavaScriptにも変更を加えます。

const accordionHeaders = document.querySelectorAll('.accordion-header');
accordionHeaders.forEach((accordionHeader) => {
    accordionHeader.addEventListener('click', () => {
        accordionHeader.classList.toggle('active');
        const accordionContent = accordionHeader.nextElementSibling;

        if (accordionContent.classList.contains('active')) {
            accordionContent.style.height = 0;
            accordionContent.classList.remove('active');
        } else {
            //styleの高さがauto(値が明示されてない)。アニメーション機能させるために、heightの値を明示
            accordionContent.style.height = accordionContent.scrollHeight + "px";
            accordionContent.classList.add('active');
        }
    });
});

accordionContentつまり<div class="accordion-content">のクラス名が、”accordion-content“の時は”active”を付けて、”accordion-content active“に変えています。
逆に、”accordion-content active“の場合は、クラス名の”active”を取り除き、”accordion-content“に戻しています。

クラス名から、”active”を取り除くときに要素の高さを0にしています。また、”active”を追加するときに高さを”scrollHeight”にしています。
”scrollHeight”プロパティは、要素の中身の高さにmarginとpaddingを加えた高さを意味しています。

また、CSSで書いたtransitionプロパティでクラス名を変更したときの変化の時間と変化の様子を設定することができます。
下図は、ease-in, ease-out, ease-in-out, それぞれの変化のイメージ図です。”transition: hight 1s ease-in, background-color 3s ease-in-out;“では、高さ(height)が変化した場合は,1秒かけてease-in特性で変化して、背景色(background-color)が変化した場合は,3秒かけてease-in-out特性で変化するというプロパティです。

下図は、完成したコードの動作画像です。CSSとJavaScriptでサクッと書いた簡単なものですが、ちょっとしたアニメーションになっています。

 

応用したコード②

基本的な実装で示したコードを書き換えて、今度は「アコーディオンの中に画像を配置して、クリック時に展開するコード」を作成していきます。基本的な実装で示したコードのDIVの中身を画像に変え、displayプロパティでの制御からclassListプロパティの制御で表示⇔非表示を切り替えるようにしています。このコードでは、タイトルをクリックすると画像や動画が表示されます。

まずは、スタイルタグに以下の変更を加えていきます。

.accordion-content {
    /*display: none;*/
    overflow: hidden;
    height: 0;/*50px => 0*/
    transition: height 3s ease-out; /* 追加 */
    margin: 0px; /* padding => margin */
    border: solid 1px #ccc;
    border-top: none;
}

/*以下、追加*/
.accordion-content img {
    width: 100%; 
    height: 300px;
}

accordion-contentクラスのDIVを画像に変換します。

<div class="accordion-container">
    <div class="accordion">
        <div class="accordion-header">タイトル1</div>
        <div class="accordion-content"><img src="./img/test1.png"></div>
    </div>
    <div class="accordion">
        <div class="accordion-header">タイトル2</div>
        <div class="accordion-content"><img src="./img/test2.png"></div>
    </div>
    <div class="accordion">
        <div class="accordion-header">タイトル3</div>
        <div class="accordion-content"><img src="./img/test3.png"></div>
    </div>
</div>

最後にJavaScriptを編集します。

const accordionHeaders = document.querySelectorAll('.accordion-header');
accordionHeaders.forEach((accordionHeader) => {
    accordionHeader.addEventListener('click', () => {
        accordionHeader.classList.toggle('active');
        const accordionContent = accordionHeader.nextElementSibling;

        if (accordionContent.classList.contains('active')) {
            accordionContent.classList.remove('active');
        } else {
            accordionContent.classList.add('active');
        }
    });
});

作成したコードの動作画像です。タイトルをクリックするとお洒落な画像が下に表示されます。今回は画像ですがDIVの中に動画を埋め込めばタイトルの動画を視聴できるWebサイト等の作成もできそうですね。

 

応用したコード③

基本的な実装で示したコードを書き換えて、今度は「階層構造のメニューの作成」を行っていこうと思います。階層構造にしたことで少し複雑になっていますが、やっていることはいままでと同じことをやっています。

まずはスタイルタグの中身を以下のように書き換えます。

<style>
    .accordion-container {
        width: 50%;
        margin: 50px auto;
    }

    .accordion-header {
        cursor: pointer;
        background-color: #eee;
        padding: 5px 10px;
        border: none;
        border-bottom: solid 1px #ccc;
        outline: none;
    }

    .accordion-header.active {
        background-color: #ccc;
    }

    .accordion {
        margin-left: 20px;
    }

    .nested-accordion-header {
        cursor: pointer;
        background-color: #eee;
        padding: 5px 10px;
        border: none;
        border-bottom: solid 1px #ccc;
        outline: none;
    }

    .nested-accordion-header.active {
        background-color: lightblue;
    }

    .accordion-content {        
        overflow: hidden;
        height: 0px;
        transition: height 1s ease-out;
        margin: 0;
        border: solid 1px #ccc;
        border-top: none;
    }

    .accordion-content.active {
        height: auto;
    }
</style>

次にBODYの<div class=“accordion-container”>タグ内を以下のように書き換えます。

<div class="accordion-container">
    <div class="accordion">
        <div class="accordion-header">タイトル1</div>
        <div class="accordion-content">
            <div class="accordion">
                <div class="nested-accordion-header">サブタイトル1</div>
                <div class="accordion-content">
                    <p>内容1-1</p>
                </div>
            </div>
            <div class="accordion">
                <div class="nested-accordion-header">サブタイトル2</div>
                <div class="accordion-content">
                    <p>内容1-2</p>
                </div>
            </div>
        </div>
    </div>
    <div class="accordion">
        <div class="accordion-header">タイトル2</div>
        <div class="accordion-content">
            <div class="accordion">
                <div class="nested-accordion-header">サブタイトル1</div>
                <div class="accordion-content">
                    <p>内容2-1</p>
                </div>
            </div>
            <div class="accordion">
                <div class="nested-accordion-header">サブタイトル2</div>
                <div class="accordion-content">
                    <p>内容2-2</p>
                </div>
            </div>
        </div>
    </div>
</div>

最後に、JavaScript部分の書き換えを行います。

<script type="text/javascript">
    const accordionHeaders = document.querySelectorAll('.accordion-header');
    accordionHeaders.forEach((accordionHeader) => {
        accordionHeader.addEventListener('click', () => {
            accordionHeader.classList.toggle('active');
            const accordionContent = accordionHeader.nextElementSibling;
            if (accordionContent.classList.contains('active')) {
                accordionContent.classList.remove('active');
            } else {
                accordionContent.classList.add('active');
            }
        });
    });

    const nestedAccordionHeaders = document.querySelectorAll('.nested-accordion-header');
    nestedAccordionHeaders.forEach((nestedAccordionHeader) => {
        nestedAccordionHeader.addEventListener('click', () => {
            nestedAccordionHeader.classList.toggle('active');
            const nestedAccordionContent = nestedAccordionHeader.nextElementSibling;
     
            if (nestedAccordionContent.classList.contains('active')) {
               nestedAccordionContent.style.height = 0;
               nestedAccordionContent.classList.remove('active');
            } else {
                nestedAccordionContent.style.height = nestedAccordionContent.scrollHeight + "px";
                nestedAccordionContent.classList.add('active');
            }
        });
    });
</script>

上記コードの”accordionContent.classList.add('active');“の部分では、クラス名に”active”を追加していますが、”.accordion-content.active { height: auto;}”で高さがauto(不確定)となるため、”transition: height 1s ease-out;”のアニメーションが適用されません。

↓が完成コードの動作している動画になります。

 

まとめ

何個か作成例を挙げまましたが、要点をまとめると以下の内容に要約できます。

①変化前後のスタイルをCSSに記述

.element {
    /*変化前のスタイル*/
    /*アニメーション(easeプロパティ等)*/
}

.element.active {
    /*変化後のスタイル*/
}

②HTMLの作成

<div>
   <div class="button">Button</div>
   <div class="element">内容1</div>
</div>
<div>
   <div class="button">Button</div>
   <div class="element">内容2</div>
</div>
<div>
   <div class="button">Button</div>
   <div class="element">内容3</div>
</div>

③JavaScriptでClassListでボタンをクリック時にクラス名を変更する仕組みを実装

const buttons = document.querySelectorAll('.button');

buttons.forEach(button) => {
    button.addEventListenr('click', ()=> {
        const element = button.nextElementSibling;

        if(element.classList.contains('active')){
            element.classList.remove('active');
        }else{
            element.classList.add('active');
        }
    });
}

スタイルシートで変更前後のプロパティを工夫したりクリックイベント発生時に他の要素のプロパティも変更してみる等、アイデア次第で動きがあって面白い独自のアコーディオンメニューも作成することができます。是非オリジナルのアコーディオンメニューを作ってみてください♪