このサイトは、jQueryなどUIのライブラリは一切使ってません。jQueryも、必要なものだけダウンロードできるならこだわらず使うかもしれないのですが、そうはできないため不要に読み込み分だけは重くなります。jQueryはブラウザ互換が保たれた素晴らしいライブラリだとは思いますが、IEのサポートを考えなくて良ければブラウザ互換の問題がかなり軽減され、いずれお役御免になるだろうとも考えていて、素のJavaScriptで書く、ということに慣れたいと考えています。
ただクリックで開閉するアコーディオンメニューだけは苦戦したので、メモも兼ねて残しておきます。
もちろん検索して先駆者の方々の作品を探してみたのですが、達成したい仕様が満たされなかったり、実装の仕方が悪いのか挙動がおかしかったりしたので、探して得た情報も参考にしつつ、自作しました。

素のJavaScriptでアコーディオンを作る(クリックで開閉・jQueryなし)

実装結果デモ

まずは実装した結果をご確認ください。
後述する仕様のうち、アコーディオンを開いた時にスクロールを許容する部分はスクロールバーが存在するCODEPENだと表現が難しかったので、その点は別途ご自分でご確認いただければと思います。

See the Pen Accordion by Takahiro Inada (@tkhr1) on CodePen.

余談

次の画像は前のアイキャッチ画像で、MicrosoftのClarityというサービスのヒートマップです。色がついている箇所がクリックされていることを示しています。このキャプチャ撮る前に画像を変えてしまったので、合成で再現してますが、大体こんな感じだったはずです。いや、みなさんせっかちすぎでしょ!クリックできそうですか??
ってことで、↑のデモはもう少し下に置いてたんですが、申し訳ない気持ちになりまして、順番を変えました。

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

アコーディオンメニューは、クリックで開閉し、開いた時だけ中のコンテンツを表示させるUIのことを指します。

アコーディオンメニューのメリットは?

ページ内に多くの情報が表示されていると、どの情報に注目したらよいのかが分かりにくくなります。ユーザーがより詳しく欲しいと思う情報を取捨選択できるようになるというメリットがあると言えます。

表示領域上の問題、つまりデザインのために用いるケースもあると思います。

アコーディオンのデメリットは?

開かないと表示されない情報は、ユーザーが開かない限りは気づかれないわけなので、その点がデメリットと言えます。そのことも踏まえ、クリックする部分に記載するメニュー名を分かりやすくし、そのメニューを見たいと思う人は気づきやすくなるような見た目を考えましょう。

達成した仕様

達成したかった(達成した)仕様は以下の通りです。

  1. 1画面内に複数のアコーディオンメニューを配置可能
  2. 1つ開いたら、他は閉じる
  3. スムーズに開閉するアニメーション
  4. 開いた時の高さは指定しなくても自動的に取得できるようにする
  5. 開いた時にスクロールを許容する
  6. 開閉のためのクリッカブルな箇所は<button>で実装する

仕様の理由

1画面内に複数のアコーディオンメニューを配置

本サイトでは、こちらのサンプルページに2か所のアコーディオンメニューがあります。1か所はPC画面の幅におけるファーストビューで表示されるグローバルメニュー、もう1か所は、スマートフォンなどで表示したときに表示されるハンバーガーメニュー押下時のグローバルメニューです。

1つ開いたら、他は閉じる

複数開いたままにできる仕様と、1つ開いたら他は閉じる仕様の両方が存在すると思います。両方開いて見比べたいとか、場合によっては前者の仕様の方が望ましいこともあるかもしれませんが、少なくともこのサイトのアコーディオンメニューは後者にした方が見やすいだろうと私は考えました。
ただ後者の方は「1つでも開いたら他は閉じる」ということをするわけなので、記録しておくべき情報が1種類増えて、実装難度は上がります。

スムーズに開閉するアニメーション

これはもはや当然ですが、スムーズに開閉するアニメーションを実装しています。また、開閉時にアイコンに変化をつけるということもしています。

開いた時の高さは自動的に取得できるようにする

アコーディオンメニューは、heightを0にすることで閉じ、開く分の高さを指定して開きます。CSSやJavaScriptのアニメーションでは、height: 0からheight: autoは変化がつかないので、開閉時のアニメーションを実装するには、開いた時の高さは指定する必要があります。あらかじめ高さを把握して、指定してしまう方法もありますが、それだと汎用性がありません。
(ちなみに、その方法であればJavaScriptを用いずにチェックボックスとCSSアニメーションだけを用いて実装する方法もありましたが、iOSでの表示がおかしかったのもあり、やめています)

開いた時にスクロールを許容する

スマートフォンのハンバーガーメニューで、アコーディオンメニューを開いたら縦の表示領域が足りなくなるため、スクロールを許容する必要がありました。

開閉のためのクリッカブルな箇所はbuttonタグで実装する

ボタンを<div><span>で実装する方法も見かけますが、以下2つの理由で、アクセシビリティのため避けた方がよいです。

1)キーボード操作できなくなり得る

<div><span>ではtabIndexをつけない限りキーボード操作ができません。tabIndexをつけたとしても、Enterボタンで押下できるようにするには、keyupイベントハンドラを実装する必要もあります。

2)スクリーンリーダーによる解釈が難しくなる

スクリーンリーダー<button>はボタンであると把握して、その旨をユーザーに伝えます。しかし<div>はどうでしょうか。試しに現時点でどうであるかNVDAで試したところ、tabIndexをつけて、onClick属性をつけてクリック可能にした部分は、「ボタン、クリック可能です」と読み上げました。まあよくできたスクリーンリーダーですが、全てのスクリーンリーダーがそうであるとは限りません。

以上のようなことから、やはりHTMLの仕様に基づき、押せる部分は<button>で実装すべきです。先述のチェックボックスを用いるという方法もありますが、チェックボックスにチェックを入れたらメニューが表示される、という仕様があまり一般的ともいえないので、その意味でもJavaScriptを用いて<button>で実装することにしました。

説明

HTML

<ul class="include-accordion scroll-control">
  <li>
    <button class="accordionBtn" type="button">Menu-1</button>
    <ul>
      <li>List 1-1</li>
      <li>List 1-2</li>
      <li>List 1-3</li>
    </ul>
  </li>
  <li>
    <button class="accordionBtn" type="button">Menu-2</button>
    <ul>
      <li>List 2-1</li>
      <li>List 2-2</li>
      <li>List 2-3</li>
    </ul>
  </li>
  <li>
    <button class="accordionBtn" type="button">Menu-3</button>
    <ul>
      <li>List 3-1</li>
      <li>List 3-2</li>
      <li>List 3-3</li>
    </ul>
  </li>
</ul>
  
<ul class="include-accordion scroll-control">
  <li>
    <button class="accordionBtn" type="button">Menu-1</button>
    <ul>
      <li>List 1-1</li>
      <li>List 1-2</li>
      <li>List 1-3</li>
    </ul>
  </li>
  <li>
    <button class="accordionBtn" type="button">Menu-2</button>
    <ul>
      <li>List 2-1</li>
      <li>List 2-2</li>
      <li>List 2-3</li>
    </ul>
  </li>
  <li>
    <button class="accordionBtn" type="button">Menu-3</button>
    <ul>
      <li>List 3-1</li>
      <li>List 3-2</li>
      <li>List 3-3</li>
    </ul>
  </li>
</ul>
  • アコーディオンメニューがある一番外の<ul>include-accordionのクラス名をつけます
    (1行目、28行目)
  • アコーディオンが開いた時にスクロールを許容したい要素にscroll-controlのクラス名をつけます
    (1行目、28行目)
  • アコーディオン開閉のためのクリック箇所となる<button>accordionBtnのクラス名をつけます
    (3行目、38行目)

クラス名は好きに変えて大丈夫です。
また、<ul>である必要はなく、例えば<div>でも同様に動きます。

CSS

ul{
  background-color: #35924A;
  width: 150px;
  padding: 0;
  color: #fff;
  float: left;
  margin-left:30px;
}

li{
  list-style: none;
}

ul ul{
  height: 0;
  padding: 0;
  overflow: hidden;
  transition: .5s;
  border-top: 1px solid #67a863;
  background-color: #5EAA6C;
  margin:0;
}

ul li li{
  border-bottom: 1px dotted #7FBF8B;
  padding: 10px 0 10px 10px;
  margin-left:15px;
}

ul:nth-of-type(1) li.active li:last-child{
  border-bottom:1px solid #67a863; 
}

button{
  position: relative;
  border: none;
  width: 100%;
  background-color: inherit;
  color: #fff;
  cursor: pointer;
  text-align: left;
  padding: 15px 0 15px 20px;
  font-size:1em;
}
button:hover{
  background-color: #1A5B27;
}

button::before,
button::after{
  content:"";
  position: absolute;
  top: 20px;
  width: 1.5px;
  height: 8px;
  background-color: #fff;
  transition: .5s;
}

button::before{
  transform: rotate(-45deg);
  right: 35px;
}

button::after{
  transform: rotate(45deg);
  right: 30px;
}

li.active button::before{
  transform: rotate(-135deg);
  transition:.5s;
}

li.active button::after{
  transform: rotate(135deg);
  transition:.5s;
}

ul:nth-of-type(2){ background-color: #357D87; }
ul:nth-of-type(2) ul{ background-color: #519FA5; border-top: 1px solid #5D9FA8; }
ul:nth-of-type(2) button:hover{ background-color: #1C4B56; }
ul:nth-of-type(2) li li{ border-bottom: 1px dotted #73BEBF; }
ul:nth-of-type(2) li.active li:last-child{ border-bottom:1px solid #5D9FA8; }

ul.active{ overflow-y: auto; }
  • 開閉する部分の初期の高さを0にして表示されないようにします(15行目)
  • 開閉する部分が表示された際にはみ出した分が表示されないようにします(17行目)
  • 閉じたときのアニメーションをCSSのtransitionでつけます(18行目)
  • 開閉のアイコンを疑似要素で作り、CSSのアニメーションで動かします(49~77行目)
    ※この部分は疑似要素でなくても問題はなく、自由に作って大丈夫です
  • 開いているときのulに対してスクロール許可します(86行目)

関係する部分は上記の通りで、あとは見た目、装飾のための記述です。

JavaScript

// メニューを開く関数
const slideDown = function(el) {
  el.style.height = 'auto'; //いったんautoに
  let h = el.offsetHeight; //autoにした要素から高さを取得
  el.style.height = h + 'px';
  el.animate([ //高さ0から取得した高さまでのアニメーション
    { height: 0 },
    { height: h + 'px' }
  ], {
    duration: 300, //アニメーションの時間(ms)
   });
   el.style.height = 'auto'; //ブラウザの表示幅を途中で閲覧者が変えた時を考慮してautoに戻す
};

// メニューを閉じる関数
const slideUp = function(el) {
  let h = el.offsetHeight;
  el.style.height = h + 'px';
  el.animate([
    { height: h + 'px' },
    { height: 0 }
  ], {
    duration: 300,
   });
   el.style.height = 0;
};

let activeIndex = null; //開いているアコーディオンのindex

//アコーディオンコンテナ全てで実行
const accordions = document.querySelectorAll('.include-accordion');
accordions.forEach( function(accordion) {

  //アコーディオンボタン全てで実行
  const accordionBtns = accordion.querySelectorAll('.accordionBtn');
  accordionBtns.forEach( function(accordionBtn, index) {
    accordionBtn.addEventListener('click', function(e) {
      activeIndex = index; //クリックされたボタンを把握
      e.target.parentNode.classList.toggle('active'); //ボタンの親要素(=ul>li)にクラスを付与/削除
      const content = accordionBtn.nextElementSibling; //ボタンの次の要素(=ul>ul)
      if(e.target.parentNode.classList.contains('active')){
        slideDown(content); //クラス名がactive(=閉じていた)なら上記で定義した開く関数を実行
      }else{
        slideUp(content); //クラス名にactiveがない(=開いていた)なら上記で定義した閉じる関数を実行
      }
      accordionBtns.forEach( function(accordionBtn, index) {
        if (activeIndex !== index) {
          accordionBtn.parentNode.classList.remove('active');
          const openedContent = accordionBtn.nextElementSibling;
          slideUp(openedContent); //現在開いている他のメニューを閉じる
        }
      });
      //スクロール制御のために上位階層ulのクラス名を変える
      let container = accordion.closest('.scroll-control'); //クラス名がscroll-controlである要素を取得
      if(accordionBtn.parentNode.classList.contains('active') == false &amp;&amp; container !== null ){
        container.classList.remove('active')
      }else if (container !== null){
        container.classList.add('active')
      }
    });
  });
});

細かい部分はコメントアウトの記述で記載していますので、あとは大まかに、何をしているのかを説明します。

  • 開くときにheight: 0からいったんheight: autoにして高さを取得してからその高さを指定する、ということをしています。
  • また、開いた後、閲覧者がブラウザの幅を変えたときに高さが指定されたままだと表示が崩れるので、アニメーションが動いた後に高さをautoに戻しています。
    ※この記事を書いた当初、autoに戻さず、高さが指定された状態からheight: 0へと閉じるときはCSSのアニメーションで動かしてましたが、ブラウザの幅が変わった時に表示が崩れるということに気づき修正しています。
  • height: 0からheight: auto、あるいはその逆のアニメーションに関して、CSSではうまくつけられず(方法ご存じの方いればご連絡ください!)、JavaScriptのanimate()メソッドを利用しています。これは比較的新しい仕様ですが、2020年にSafariが対応し、IEを除く主要なブラウザでは動きます。
    ※この部分について、繰り返し処理を用いて少しずつ高さを足していく、という方法を取っているものもあると思いますが(未確認ですがjQueryはそれ?)、ブラウザに組み込まれた処理なので、それよりもパフォーマンスが良いはずです。
  • クリックされたボタンをactiveIndex(変数名なので名称は自由)で定義した変数で把握し、あるボタンがクリックされたときに、他のボタンの次の要素を閉じる、ということをしています。
    ※開いた部分は閉じない限り開いておく仕様にする場合は、この部分が不要となりますので、28行目、38行目、46~52行目を削除してください。

まとめ

jQueryなしでアコーディオンメニューを作る方法を説明しました。
求めた仕様が割と複雑ですが、決めたクラス名をつけるだけでよくて、汎用性がある状態にできたと思ったので、ご紹介しました。
ご指摘や改善点、感想などあれば問い合わせフォームからいただければ幸いです。

著者のイメージ画像

BringFlower|稲田 高洋

2003年から大手総合電機メーカーでUXデザインプロセスの研究、実践。UXデザイン専門家の育成プログラム開発。SEOにおいても重要なW3Cが定めるWeb標準仕様策定にウェブアクセシビリティの専門家として関わる。2010~2018年に人間中心設計専門家を保有。その後、不動産会社向けにSaaSを提供する企業の事業開発部で複数サービスを企画、ローンチ。CMSを提供し1000以上のサイトを分析。顧客サポート、サイト運営にも関わる。2022年3月にBringFlowerを開業し、SEOコンサル、デザイン、ウェブ制作を一手に受ける。グッドデザイン賞4件、ドイツユニバーサルデザイン賞2件、米国IDEA賞1件の受賞歴あり。