スマホでハンバーガーメニューを実装する際
- 初期状態でサブメニューは非表示
- 開閉ボタンでサブメニューを表示と非表示を切り替え
- 開閉どちらもスムーズなアニメーションを付与
という要件の案件がありました。
jQueryは使いたくなく、cssとjsだけでどうにかならないかなと思い、よくある表現ではありますが最近実装していなかったのでこれを機に汎用的なものを作ってそれを残しておこうと思いました。
CodePen
See the Pen Untitled by konno1614 (@konnotes) on CodePen.
html
<nav class="c-nav">
<ul class="c-nav__list">
<li class="c-nav__item">
<a class="c-nav__link" href="#">Main Menu 01</a>
</li>
<li class="c-nav__item">
<a class="c-nav__link" href="#">Main Menu 02</a>
<div class="c-nav__toggle-button js-toggle-plus-minus"><span></span></div>
<ul class="c-nav__child-list">
<li>
<a href="#">Sub menu 01</a>
</li>
<li>
<a href="#">Sub menu 02</a>
</li>
<li>
<a href="#">Sub menu 03</a>
</li>
</ul>
</li>
<li class="c-nav__item">
<a class="c-nav__link" href="#">Main Menu 03</a>
<div class="c-nav__toggle-button js-toggle-plus-minus"><span></span></div>
<ul class="c-nav__child-list">
<li>
<a href="#">Sub menu 04</a>
</li>
<li>
<a href="#">Sub menu 05</a>
</li>
<li>
<a href="#">Sub menu 06</a>
</li>
</ul>
</li>
</ul>
</nav>
htmlは一般的なマークアップかと思います。
css(scss)
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: #f4f4f4;
}
.c-nav {
max-width: 500px;
margin: 0 auto;
&__list {
list-style: none;
}
&__item {
position: relative;
background-color: rgba($color: #efba22, $alpha: 0.3);
padding: 20px;
& + & {
border-top: 1px solid #333;
}
}
&__link {
text-decoration: none;
color: #333;
}
&__toggle-button {
cursor: pointer;
transition: all 0.3s ease-in-out;
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
position: absolute;
top: 20px;
right: 20px;
span {
position: relative;
display: inline-block;
width: 20px;
height: 2px;
background-color: #db4527;
&::after {
content: "";
display: inline-block;
width: 2px;
height: 20px;
background-color: #db4527;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
&.is-open {
transform: rotate(180deg);
span {
&::after {
opacity: 0;
}
}
}
}
&__child-list {
list-style: none;
overflow: hidden;
max-height: 0;
opacity: 0;
margin-top: 0;
transition: max-height 0.3s ease-out, opacity 0.3s ease-out,
margin-top 0.3s ease-out;
&.is-open {
max-height: unset;
opacity: 1;
margin-top: 20px;
}
li {
& + li {
margin-top: 10px;
}
a {
text-decoration: none;
color: #1f32ad;
}
}
}
}
cssは最低限の見た目になるように調整しています。
67行目のtransitionの記述が大事になります。
js(typescript)
//Plus or Minus
function plusMinusButton() {
const toggleButtons = document.querySelectorAll(".js-toggle-plus-minus");
toggleButtons.forEach((button) => {
button.addEventListener("click", () => {
button.classList.toggle("is-open");
const parent = button.closest(".c-nav__item");
if (parent) {
const childList = parent.querySelector(
".c-nav__child-list"
) as HTMLElement;
if (childList) {
if (childList.classList.contains("is-open")) {
childList.style.maxHeight = `${childList.scrollHeight}px`;
setTimeout(() => {
childList.classList.remove("is-open");
childList.style.maxHeight = "0";
}, 10);
} else {
childList.classList.add("is-open");
const scrollHeight = childList.scrollHeight;
childList.style.maxHeight = `${scrollHeight}px`;
setTimeout(() => {
childList.style.maxHeight = "none";
}, 300);
}
}
}
});
});
}
function init() {
plusMinusButton();
}
init();
jsは長くなってしまいましたが、難しいことはしていません。
まとめ
正直他の案件にも使いまわせるかは微妙ですが、ここから改善していきたいと思います。
よくある実装なので、汎用性のあるものにブラッシュアップできればと思います。