PR

height: auto;(=高さ可変)なアコーディオンメニューの作り方

JavaScriptを学ぶ

はい、今回はアコーディオンメニューについてです。
このメニューはwebデザインにおいて採用されることが多く、実装の機会も多くなります。

そこで良く出会うのが、「height: auto;にアニメーションが効かない」問題です。コーダーをやっている方なら1度はこの問題に出会うのではないでしょうか。

そんなheight: auto;なアコーディオンメニューをスマートに作る方法を紹介します。

基本的なアコーディオンメニューの作り方はこちらから。また、この記事のコードも以下の記事と同様のリセットCSS(https://github.com/nicolas-cusan/destyle.css/blob/master/destyle.css)を導入している前提で解説します。

結論

解説が長くなるので、まずは最終的なコードを載せます。

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>Document</title>
    <link rel="stylesheet" href="./resources/styles/destyle.css" />
    <link rel="stylesheet" href="./resources/styles/style.css" />
  </head>
  <body>
    <nav class="AcordionMenu">
      <ul class="Menu">
        <li class="Menu-Item">
          <span class="Menu-Item-Label" data-is-open="false">メニュー1</span>
          <div class="Menu-Item__Inner">
            <p class="Menu-Item-Content">
              ダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキスト
            </p>
          </div>
        </li>
        <li class="Menu-Item">
          <span class="Menu-Item-Label" data-is-open="false">メニュー2</span>
          <div class="Menu-Item__Inner">
            <p class="Menu-Item-Content">
              ダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキスト
            </p>
          </div>
        </li>
        <li class="Menu-Item">
          <span class="Menu-Item-Label" data-is-open="false">メニュー3</span>
          <div class="Menu-Item__Inner">
            <p class="Menu-Item-Content">
              ダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキスト
            </p>
          </div>
        </li>
        <li class="Menu-Item">
          <span class="Menu-Item-Label" data-is-open="false">メニュー4</span>
          <div class="Menu-Item__Inner">
            <p class="Menu-Item-Content">
              ダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキスト
            </p>
          </div>
        </li>
      </ul>
    </nav>
    <script
      src="https://code.jquery.com/jquery-3.6.0.slim.min.js"
      integrity="sha256-u7e5khyithlIdTpu22PHhENmPcRdFiHRjhAuHcs05RI="
      crossorigin="anonymous"
    ></script>
    <script>
      $(".Menu-Item-Label").on("click", function () {
        const label = $(this)

        if (label.attr("data-is-open") === "false") {
          const targetHeight = label.next().children().outerHeight()
          label.next().height(targetHeight)

          setTimeout(function () {
            label.next().height("auto")
            label.attr("data-is-open", "true")
          }, 500)

          return
        }

        if (label.attr("data-is-open") === "true") {
          const targetHeight = label.next().children().outerHeight()
          label.next().height(targetHeight)

          setTimeout(function () {
            label.next().height(0)
            label.attr("data-is-open", "false")
          }, 1)

          return
        }
      })
    </script>
  </body>
</html>
style.css
.AcordionMenu {
  margin-left: auto;
  margin-right: auto;
  width: 100%;
  max-width: 800px;
}

.Menu {
  width: 100%;
  background-color: #fff;
}

.Menu-Item {
  margin-bottom: 1px;
  width: 100%;
  background-color: #6ba6e7;
}

.Menu-Item-Label {
  width: 100%;
  height: 40px;
  padding-left: 16px;
  padding-right: 16px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  color: #fff;
  cursor: pointer;
}

.Menu-Item__Inner {
  overflow: hidden;
  width: 100%;
  height: 0;
  transition: all 0.5s;
}

.Menu-Item-Content {
  width: 100%;
  padding: 20px;
  background-color: #fff;
  line-height: 2;
}

解説

実装方針|一旦数値を挟んでからautoにする

早速作っていきましょう。見た目はこんな感じとします。

各メニュー項目のラベルをクリックして開閉する形です。

HTML/CSS部分は以下の通りです。

構造としてはメニューがあって、その中にメニューの各項目を配置します。

各項目はそれぞれ見出しとコンテンツを持ちます。

<nav class="AcordionMenu">
  <ul class="Menu">
    <li class="Menu-Item">
      <span class="Menu-Item-Label" data-is-open="false">メニュー1</span>
      <div class="Menu-Item__Inner">
        <p class="Menu-Item-Content">
          ダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキスト
        </p>
      </div>
    </li>
    <li class="Menu-Item">
      <span class="Menu-Item-Label" data-is-open="false">メニュー2</span>
      <div class="Menu-Item__Inner">
        <p class="Menu-Item-Content">
          ダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキスト
        </p>
      </div>
    </li>
    <li class="Menu-Item">
      <span class="Menu-Item-Label" data-is-open="false">メニュー3</span>
      <div class="Menu-Item__Inner">
        <p class="Menu-Item-Content">
          ダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキスト
        </p>
      </div>
    </li>
    <li class="Menu-Item">
      <span class="Menu-Item-Label" data-is-open="false">メニュー4</span>
      <div class="Menu-Item__Inner">
        <p class="Menu-Item-Content">
          ダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキスト
        </p>
      </div>
    </li>
  </ul>
</nav>
.AcordionMenu {
  margin-left: auto;
  margin-right: auto;
  width: 100%;
  max-width: 800px;
}

.Menu {
  width: 100%;
  background-color: #fff;
}

.Menu-Item {
  margin-bottom: 1px;
  width: 100%;
  background-color: #6ba6e7;
}

.Menu-Item-Label {
  width: 100%;
  height: 40px;
  padding-left: 16px;
  padding-right: 16px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  color: #fff;
  cursor: pointer;
}

.Menu-Item__Inner {
  overflow: hidden;
  width: 100%;
  height: 0;
  transition: all 0.5s; /*アニメーション用*/
}

.Menu-Item-Content {
  width: 100%;
  padding: 20px;
  background-color: #fff;
  line-height: 2;
}

ポイントはコンテンツ部分が2重構造になっていることで、外側(.Menu-Item__Inner)が高さ0になっていることにより初期状態で余白も含めてコンテンツを見せないようにしています。

さて、次の発想として.Menu-Item__Innerの高さを0とautoで切り替わるようにするやり方ですが、単純に切り替えるだけでは想定の動きになりません。例えば以下のようなコードになるでしょう。

付け替え用のクラスと、それを操作するスクリプトを用意します。今回はjQueryを用います。

.Menu-Item__isOpen {
  height: auto;
}
<script
  src="https://code.jquery.com/jquery-3.6.0.slim.min.js"
  integrity="sha256-u7e5khyithlIdTpu22PHhENmPcRdFiHRjhAuHcs05RI="
  crossorigin="anonymous"
></script>
<script>
  $(".Menu-Item-Label").on("click", function () {
    $(this).next().toggleClass("Menu-Item__isOpen")
  })
</script>

.Menu-Item__Innerにはきちんとtransitionの設定をしているにもかかわらず、アニメーションをせずに切り替わってしまいます(試してみてください)。

冒頭でも書いた通り、autoへのtransitionは効かないからです。

widthやheightのtransitionは基本的に数値→数値でないとうまく動きません。しかし、こういったメニューで高さが数値指定であることは実務ではあまりないのではないでしょうか。

そこで見出しにも書いたように、一旦数値を挟んでからアニメーションさせ、その後に高さをautoにするという手順を踏みましょう。

具体的には、メニューを開くときは

0→(アニメーション)→コンテンツの高さ→auto

メニューを閉じるときは

auto→コンテンツの高さ→(アニメーション)→0

というような変化をするようにしていきます。

アニメーション実装

まず、メニュー開閉状態をどこかに保持した方が都合が良いです。今回はdata属性を用いましょう。

各メニューのラベル部分(Menu-Item-Label)にdata-is-openという属性を付与します。値はfalseです。

<li class="Menu-Item">
  <span class="Menu-Item-Label" data-is-open="false">メニュー1</span> <!-- data属性を付与 -->
  <div class="Menu-Item__Inner">
    <p class="Menu-Item-Content">
      ダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキストダミーテキスト
    </p>
  </div>
</li>

jQueryからは、この値がfalseであればメニューを開く処理を行い、trueの時に閉じる処理を行います。

$(".Menu-Item-Label").on("click", function () {
  const label = $(this)

  if (label.attr("data-is-open") === "false") {
    //メニューを開く
  }

  if (label.attr("data-is-open") === "true") {
    //メニューを閉じる
  }
})

メニューの開閉は先述の通りheightを変更するわけですが、コンテンツの高さはどのように取得すれば良いでしょうか。

今回のコードでは、コンテンツ部分を2重にしており外側(.Menu-Item__Inner)の高さを0にしているため内側(.Menu-Item-Content)の高さは残っています。これを使いましょう。

まずはメニューを開く処理から。

$(".Menu-Item-Label").on("click", function () {
  const label = $(this)

  if (label.attr("data-is-open") === "false") {
    //コンテンツ内側の要素の高さを取得する。
    const targetHeight = label.next().children().outerHeight() 

    //コンテンツ外側に、内側要素の高さを設定する。ここでtransitionによるアニメーションが作動する。
    label.next().height(targetHeight) 
  }

  if (label.attr("data-is-open") === "true") {
    //メニューを閉じる
  }
})

そしてメニューを開くアニメーションが終わるタイングでheightをautoにし、data属性の値を変更します。

$(".Menu-Item-Label").on("click", function () {
  const label = $(this)

  if (label.attr("data-is-open") === "false") {
    //コンテンツ内側の要素の高さを取得する。
    const targetHeight = label.next().children().outerHeight() 

    //コンテンツ外側(高さ0)に、内側要素の高さを設定する。ここでtransitionによるアニメーションが作動する。
    label.next().height(targetHeight) 

    //transition時に指定した秒数分遅らせて、アニメーション完了後にheightをautoにする。
    //data属性も変更する。
    setTimeout(function () {
      label.next().height("auto")
      label.attr("data-is-open", "true")
    }, 500)

    return
  }

  if (label.attr("data-is-open") === "true") {
    //メニューを閉じる
  }
})

閉じる場合の処理は以下の形です。autoになっている高さを一旦数値に置き換えてから、一瞬遅らせて高さを0にします。

$(".Menu-Item-Label").on("click", function () {
  const label = $(this)

  if (label.attr("data-is-open") === "false") {
    //省略
  }

  if (label.attr("data-is-open") === "true") {
    //コンテンツ内側の要素の高さを取得する。
    const targetHeight = label.next().children().outerHeight()

    //コンテンツ外側(高さauto)に、内側要素の高さを設定する。
    label.next().height(targetHeight)

    setTimeout(function () {
      label.next().height(0)  //高さを0にする。ここでアニメーションが作動する。
      label.attr("data-is-open", "false")
    }, 1)

    return
  }
})

これでheight: auto;なアコーディオンメニューができました。

まとめ

ということで、height: auto;なアコーディオンメニューでした。

height: 0; とheight: auto; の変化をさせる際に一旦数値を挟むことがポイントでした。この点さえ押さえれば今回以外の書き方でも実現可能なので、環境に合わせてお試しください。

コメント

タイトルとURLをコピーしました