【ホバーで出る】JavaScriptのみでドロップダウンメニューを作る

読者の悩み

  • ホバーで出るドロップダウンメニューの作り方がわからない
  • アコーディオンメニューのような動きで開くアニメーションにしたい

ホバーでドロップダウンメニューの作り方がわからない...

ホバーをトリガーとしたドロップダウンメニューの作るイメージがわかりにくいですよね。
今回はそのあたりを丁寧に説明させていただきます。

本記事の内容

  • ホバーでのドロップダウンメニューの作り方を解説します
  • アコーディオンのような動きはCSSのみで実装可能

ホバーでのドロップダウンメニューの作り方を解説します

今回はHTML、CSS、JavaScriptでドロップダウンメニューを作っていきたいと思います。

まずは完成イメージです。

とりあえず、このように動くイメージで作っていきます。

まずはHTMLファイルから作っていきましょう。

HTMLファイルを作っていこう

index.htmlファイルを作り、まずはここまで書いていきます。

<!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>ドロップダウンメニュー</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <main>
   
  </main>

  <script src="index.js"></script>
</body>
</html>

bodyタグの中にmainタグを作って、headタグの中にstyles.cssの読込と、bodyの閉じタグの手前にindex.jsの読込を完了しておき、styles.cssとindex.jsのファイルを作っておいてください。

今回はEmmetを使い、一気にHTMLファイルを完成させます。

mainタグの中に、こちらを書いていきます。

ul.menu>li.menu__item*3>a.menu__link{親メニュー$}+ul.drop-menu>li.drop-menu__item*3>a.drop-menu__link{子メニュー$}

こちらをエディターに記入して、エンターで開きます。

index.htmlファイルはこのような感じになったのではないでしょうか?

<!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>ドロップダウンメニュー</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <main>
    <ul class="menu">
      <li class="menu__item">
        <a href="" class="menu__link">親メニュー1</a>
        <ul class="drop-menu">
          <li class="drop-menu__item"><a href="" class="drop-menu__link">子メニュー1</a></li>
          <li class="drop-menu__item"><a href="" class="drop-menu__link">子メニュー2</a></li>
          <li class="drop-menu__item"><a href="" class="drop-menu__link">子メニュー3</a></li>
        </ul>
      </li>

      <li class="menu__item">
        <a href="" class="menu__link">親メニュー2</a>
        <ul class="drop-menu">
          <li class="drop-menu__item"><a href="" class="drop-menu__link">子メニュー1</a></li>
          <li class="drop-menu__item"><a href="" class="drop-menu__link">子メニュー2</a></li>
          <li class="drop-menu__item"><a href="" class="drop-menu__link">子メニュー3</a></li>
        </ul>
      </li>

      <li class="menu__item">
        <a href="" class="menu__link">親メニュー3</a>
        <ul class="drop-menu">
          <li class="drop-menu__item"><a href="" class="drop-menu__link">子メニュー1</a></li>
          <li class="drop-menu__item"><a href="" class="drop-menu__link">子メニュー2</a></li>
          <li class="drop-menu__item"><a href="" class="drop-menu__link">子メニュー3</a></li>
        </ul>
      </li>
    </ul>
  </main>

  <script src="index.js"></script>
</body>
</html>

これで、index.htmlは完成となります。

Emmetを導入されてない人は、すいませんが上記を参考に入力してください。

HTMLのポイントは、親メニューの中に子メニューを含めてしまっていることです。こういうふうに設定することで、親メニューにホバー設定をすることで、子メニューにホバーしていても、子メニューが消えないようになります。

子メニューも親メニューの一部ですからね。

ブラウザで表示をみてみましょう。

表示はこのようになってれば、OKです。

それでは、次はCSSでスタイルをつけていきましょう。

CSSを書いていこう

styles.cssファイルに下記を書いていってください。

*, *::after, *::before {
  box-sizing: border-box;
}

body {
  margin: 0;
  padding: 0;
}

a {
  color: #333;
  text-decoration: none;
}

ul {
  padding: 0;
  margin: 0;
}

li {
  list-style: none;
  padding: 0;
  margin: 0;
}

main {
  width: 100%;
  margin: 0px auto 10px;
  background-color: lightcyan;
}

.menu {
  display: flex;
  justify-content: center;
}

.menu__link {
  display: block;
  padding: 10px 20px;
}

.menu__link:hover {
  background-color: #ebebeb;
  color: #666;
}

.drop-menu {
  position: absolute;
  top: 43px;
  transition: all .3s;
}

.drop-menu__link {
  display: block;
  /* display: none; デフォルトの設定は消す */ 
  background-color: #ebebeb;
  transition: all .3s;
  padding: 5px 20px;
}

.drop-menu__link:hover {
  background-color: lightcyan;
}

親メニューと子メニューにスタイルをつけています。

ポイントは、55行目の.drop-menu__linkは子メニューのaタグですが、display: none; でデフォルトでは消していますが、今はみえるようにしています。

ブラウザで表示をみてみましょう。

このようになってればOKです。

表示は確認できたので、55行目の「display: none;」を有効にしてください。

次は、ホバーした時のドロップダウンのスタイルを付けていきます。

styles.cssの1番最後に下記のCSSを追記してください。

/* ドロップダウン出現後のスタイル */
.drop-menu__link.is-active {
  display: block;
}

これは、JavaScriptで、.drop-menu__linkに「is-active」クラスを付加して、display: block;で非表示の状態を表示させることができます。

styles.cssの全体はこのようになります。

*, *::after, *::before {
  box-sizing: border-box;
}

body {
  margin: 0;
  padding: 0;
}

a {
  color: #333;
  text-decoration: none;
}

ul {
  padding: 0;
  margin: 0;
}

li {
  list-style: none;
  padding: 0;
  margin: 0;
}

main {
  width: 100%;
  margin: 0px auto 10px;
  background-color: lightcyan;
}

.menu {
  display: flex;
  justify-content: center;
}

.menu__link {
  display: block;
  padding: 10px 20px;
}

.menu__link:hover {
  background-color: #ebebeb;
  color: #666;
}

.drop-menu {
  position: absolute;
  top: 43px;
  transition: all .3s;
}

.drop-menu__link {
  display: none; /* display: block;は削除 */
  background-color: #ebebeb;
  padding: 5px 20px;
}

.drop-menu__link:hover {
  background-color: lightcyan;
}

/* ドロップダウン出現後のスタイル */
.drop-menu__link.is-active {
  display: block;
}

これでCSSファイルは完成になります。

これで、一度ブラウザで表示を確認してみましょう。

ドロップダウンメニューが消えましたね。

それでは、次はJavaScriptでドロップダウンの動きをつけていきたいと思います。

JavaScriptでドロップダウンメニューに動きを付けていこう

index.jsファイルにJavaScriptを書いていきます。

JavaScriptを書く手順です。

  • 親メニュー(liタグ)を取得
  • 親メニュー(liタグ)すべてにクリックイベントを付ける
  • イベント発動時の処理を書く

という流れになります。

まずは親メニューの取得ですね。

index.jsファイルに書いていきましょう。

'use strict';

{
  // DOM取得
  // 親メニューのli要素
  const parentMenuItem = document.querySelectorAll('.menu__item');
  console.log(parentMenuItem);
}

まずは、querySelectorAllで親メニューのli要素をすべて取得します。

console.logで確認してみましょう。

NodeListで3つのli要素を取得できてますね。
NodeListがわからない人は、下記記事で解説してます。

親メニューのli要素を取得できたので、次はイベントを親メニューのli要素すべてに付けていきたいと思います。

index.jsファイルの「console.log(parentMenuItem);」のすぐ下に追記してください。

// イベントを付加
  for (let i = 0; i < parentMenuItem.length; i++) {
    parentMenuItem[i].addEventListener('mouseover', dropDownMenuOpen);
  }

for文で、親メニューのli要素すべてにイベントを付けていきます。

今回のイベントは、「'mouseover'」です。イベントを付けた要素にマウスカーソルがホバーすると、処理が発動します。

index.jsファイルの全体はこのようになります。

'use strict';

{
  // DOM取得
  // 親メニューのli要素
  const parentMenuItem = document.querySelectorAll('.menu__item');
  console.log(parentMenuItem);

  // イベントを付加
  for (let i = 0; i < parentMenuItem.length; i++) {
    parentMenuItem[i].addEventListener('mouseover', dropDownMenuOpen);
  }

}

これで、イベント付加まで完了しました。

次はイベント発動時の処理を書いていきます。

index.jsファイルのfor文の下に追記していってください。

  // ドロップダウンメニューを開く処理
  function dropDownMenuOpen(e) {
    // 子メニューa要素
    const childMenuLink = e.currentTarget.querySelectorAll('.drop-menu__link');
    console.log(childMenuLink);

    // is-activeを付加
    for (let i = 0; i < childMenuLink.length; i++) {
      childMenuLink[i].classList.add('is-active');
    }

  }

イベント付加の部分で、関数名(dropDownMenuOpen)を指定したので、その関数を別で書いています。

dropDownMenuOpen(e)となっていますが、eにはイベントが発動した要素の情報が入ってきています。

なので、「e.currentTarget」とイベントの中で書くことで、マウスホバーした要素を取得できます。

今回の処理だったら、マウスホバーした親メニューのli要素にquerySelectorAll('.drop-menu__link')しているので、マウスホバーしたli要素の中だけの、aタグを取得できます。

次は、取得したaタグを処理するために、for文の繰り返し処理で、マウスホバーした親メニューのli要素の中のaタグすべてに「is-active」クラスを付けています。

このfunctionですが、別の方法としては、親メニューのli要素ごとに違うクラス名を付けることで、個別で処理できますが、そんなめんどくさいことをするのも嫌ですし、コードがごちゃごちゃになります。

e.currentTargetでホバーした要素を特定する方法だと、急遽メニュー自体が増えた時にも、クラス名をそのままで、簡単にコピペで追加することができますし、コードがシンプルでみやすくなっています。

index.jsファイルの全体をみていきましょう。

'use strict';

{
  // DOM取得
  // 親メニューのli要素
  const parentMenuItem = document.querySelectorAll('.menu__item');
  console.log(parentMenuItem);

  // イベントを付加
  for (let i = 0; i < parentMenuItem.length; i++) {
    parentMenuItem[i].addEventListener('mouseover', dropDownMenuOpen);
  }

  // ドロップダウンメニューを開く処理
  function dropDownMenuOpen(e) {
    // 子メニューa要素
    const childMenuLink = e.currentTarget.querySelectorAll('.drop-menu__link');
    console.log(childMenuLink);

    // is-activeを付加
    for (let i = 0; i < childMenuLink.length; i++) {
      childMenuLink[i].classList.add('is-active');
    }

  }

}

これで、ドロップダウンメニューを開けるようになったと思います。

こんな感じで、ホバーでドロップダウンメニューが出てくれば、OKです。

ドロップダウンが出てくるのはいいですが、ホバーが外れたら、ドロップダウンメニューはまた消えてほしいですよね。

次はホバーが外れた時の処理を書いていきます。

ドロップダウンメニューをホバーが外れると消えるイベントを追加します。

index.jsのイベント付加の部分に、下記の1行を追加します。

 parentMenuItem[i].addEventListener('mouseleave', dropDownMenuClose);

ホバーした要素に、'mouseleave'というイベントを付けています。
leaveの意味は「離れる」ですので、ホバーから離れたら発動するイベントとなります。

index.jsファイル全体をみていきましょう。

'use strict';

{
  // DOM取得
  // 親メニューのli要素
  const parentMenuItem = document.querySelectorAll('.menu__item');
  console.log(parentMenuItem);

  // イベントを付加
  for (let i = 0; i < parentMenuItem.length; i++) {
    parentMenuItem[i].addEventListener('mouseover', dropDownMenuOpen);
    parentMenuItem[i].addEventListener('mouseleave', dropDownMenuClose);
  }

  // ドロップダウンメニューを開く処理
  function dropDownMenuOpen(e) {
    // 子メニューa要素
    const childMenuLink = e.currentTarget.querySelectorAll('.drop-menu__link');
    console.log(childMenuLink);

    // is-activeを付加
    for (let i = 0; i < childMenuLink.length; i++) {
      childMenuLink[i].classList.add('is-active');
    }

  }

}

イベント付加の中に1行追加しました。

それでは、'mouseleave'イベントの関数を定義していきます。

それでは、index.jsファイルの27行目の部分に「dropDownMenuClose」関数を定義していきます。

  // ドロップダウンメニューを閉じる処理
  function dropDownMenuClose(e) {
    // 子メニューリンク
    const childMenuLink = e.currentTarget.querySelectorAll('.drop-menu__link');
    console.log(childMenuLink);

    // is-activeを削除
    for (let i = 0; i < childMenuLink.length; i++) {
      childMenuLink[i].classList.remove('is-active');
    }
   }

関数のスコープの関係で、dropDownMenuOpenの関数内の子メニューリンク(aタグ)部分の変数は持ち出せないので、もう一度同じように、ホバーした要素の子メニューリンク(aタグ)をもう一度、変数に入れ直しています。

そして、ホバーした要素の子メニューリンク(aタグ)をfor文で取り出してます。
ほぼ一緒なんですが、最後のaタグのis-activeクラスをremoveで削除する部分がこのイベントの違いになります。

index.jsファイル全体をみていきましょう。

'use strict';

{
  // DOM取得
  // 親メニューのli要素
  const parentMenuItem = document.querySelectorAll('.menu__item');
  console.log(parentMenuItem);

  // イベントを付加
  for (let i = 0; i < parentMenuItem.length; i++) {
    parentMenuItem[i].addEventListener('mouseover', dropDownMenuOpen);
    parentMenuItem[i].addEventListener('mouseleave', dropDownMenuClose);
  }

  // ドロップダウンメニューを開く処理
  function dropDownMenuOpen(e) {
    // 子メニューa要素
    const childMenuLink = e.currentTarget.querySelectorAll('.drop-menu__link');
    console.log(childMenuLink);

    // is-activeを付加
    for (let i = 0; i < childMenuLink.length; i++) {
      childMenuLink[i].classList.add('is-active');
    }

  }

  // ドロップダウンメニューを閉じる処理
  function dropDownMenuClose(e) {
    // 子メニューリンク
    const childMenuLink = e.currentTarget.querySelectorAll('.drop-menu__link');
    console.log(childMenuLink);

    // is-activeを削除
    for (let i = 0; i < childMenuLink.length; i++) {
      childMenuLink[i].classList.remove('is-active');
    }

  }

}

これで、マウスホバーが外れるとis-activeクラスが削除されて、ドロップダウンメニューが消えるようになったと思います。

ブラウザチェックをしてみましょう。

ホバーを外すとドロップダウンメニューが消えるようになりました。

メニューを新たに3つから4つに変更しても同じように機能するので、試してみてください。あと、index.jsのconsole.logは取得状況を確認したら削除しておきます。

これでドロップダウンの機能的にはほぼ完成なんですが、アニメーションを入れてスムーズにドロップダウンメニューを出していきましょう。

アコーディオンのような動きはCSSのみで実装可能

ドロップダウンが出てくる時に、アコーディオンのように広がるようにアニメーションさせてみましょう。

この実装はCSSのみで可能です。

styles.cssファイルを変更していきましょう。

下記CSSのように、現在のCSSを変更してください。

*, *::after, *::before {
  box-sizing: border-box;
}

body {
  margin: 0;
  padding: 0;
}

a {
  color: #333;
  text-decoration: none;
}

ul {
  padding: 0;
  margin: 0;
}

li {
  list-style: none;
  padding: 0;
  margin: 0;
}

main {
  width: 100%;
  margin: 0px auto 10px;
  background-color: lightcyan;
}

.menu {
  display: flex;
  justify-content: center;
}

.menu__link {
  display: block;
  padding: 10px 20px;
}

.menu__link:hover {
  background-color: #ebebeb;
  color: #666;
}

.drop-menu {
  background-color: #ebebeb; /* 追記 */
  position: absolute;
  top: 43px;
  transition: all .3s;
}

.drop-menu__link {
  display: block; /* display: block;に変更 */
  background-color: #ebebeb;
  padding: 0px 20px; /* 5pxを0pxに変更 */
  line-height: 0; /* 追記 */
  transform: scaleY(0); /* 追記 */
  transition: all .3s; /* 追記 */
}

.drop-menu__link:hover {
  background-color: lightcyan;
}

/* ドロップダウン出現後のスタイル */
.drop-menu__link.is-active {
  /* display: blockを削除 */
  padding: 5px 20px; /* 追記 */
  line-height: 1.6; /* 追記 */
  transform: scaleY(1); /* 追記 */
}

このCSSはどのように変更しているかというと、

変更前は、display:none;からdisplay: block;に変更することで要素を出したり消したりしてましたが、それをやめて、子メニューのaタグ要素の高さを出すCSSをすべて0にして、ドロップダウンメニューを初期状態で消しています。

padding、line-height、transform: scaleYですべての高さを0にしてます。そして、is-activeを付加することですべてのCSSの高さを戻します。

そうすると、アコーディオンのようにアニメーションが可能になります。

ブラウザチェックしてみましょう。

いい感じでアコーディオンのようなアニメーションになってますね。

このCSSを利用すれば、アコーディオンメニューも再現可能です。

これで、ドロップダウンメニューが完了しました。

今回の内容でわからない部分があれば、完成のデータをこちらからダウンロードできます。

今回はこれで以上になります。

もし実装などで上手くいかないことありましたら、下記ボタンからご相談いただけましたらありがたいです。

この記事を書いた人

アバター

トノムラマサシ

masashi
Webサイト屋兼ブロガー|過去に企業常駐などを経て現在はフリーランスでディレクションとコーダーとして活動しております。JS大好きなのでJSの仕事依頼お待ちしております。京都在中で5人家族。