【アニメーション&複数対応】アコーディオンメニューをJavaScriptで作る(サンプルコードあり)
はじめに
この記事では、以下条件に対応出来るアコーディオンメニューの作り方を解説します。
- JavaScriptのみを使用(jQueryは使わない)
- 表示非表示の切り替えがアニメーション(jQueryでいうslideDown的なイメージ)
- アコーディオンする要素は「ひとつ」でも「複数」でもどちらも対応可能
- 同一ページで複数の要素に対してアコーディオンを設定出来る
動作サンプル
実際のコード
まずは、サンプルコードを下記に載せます。
JavaScript、HTML、CSSをコピーすれば、そのまま使えます。
JavaScript
function Accordion(obj) {
// `wrap_id`で指定した要素が無かったら、以降は実行しない
if (!document.querySelector(`#${obj.wrap_id}`)) {
return false
}
// 関数呼び出し時の引数をもとに変数を定義
const ElmItems = document.querySelectorAll(`#${obj.wrap_id} .${obj.item_class}`)
const ClassAccordionElm = obj.accordion_class
const ElmAccordions = document.querySelectorAll(`#${obj.wrap_id} .${ClassAccordionElm}`)
const Transition = obj.accordion_transition ? obj.accordion_transition : '.4s'
const AttrToggle = obj.toggle_attr ? obj.toggle_attr : 'data-active'
const DefaultOpen = obj.default_is_open ? obj.default_is_open : false
let arryAccordionElmHeight = new Array()
// アコーディオンさせる要素の高さをそれそれ取得して配列に格納
// その後、`default_is_open`が`false`なら高さを0にする
Array.from(ElmAccordions).forEach(element => {
element.style.height = 'auto'
element.style.overflow = 'hidden'
element.style.transition = Transition
arryAccordionElmHeight.push(element.clientHeight)
if (!DefaultOpen) {
element.style.height = '0'
}
})
Array.from(ElmItems).forEach((element, index) => {
// `default_is_open`が`true`なら、
// `item_class`にアコーディオンが開いている時に付与する`toggle_attr`の属性を設定する
// (アコーディオンが閉じている時は、`toggle_attr`の属性は削除される)
// アコーディオンする要素に`height`を設定する(autoから0にすると`transition`を設定してもアニメーションしないから、`height`を数値で設定する)
if (DefaultOpen) {
element.setAttribute(AttrToggle, '')
ElmAccordions[index].style.height = `${arryAccordionElmHeight[index]}px`
}
// `item_class`に`addEventListener`を設定
element.addEventListener('click', (event) => {
// クリック時の要素と属性の有無を取得
// (クリック時に`toggle_attr`の属性を付け外しすることで、アコーディオンが開いているかどうかを判別する)
let target = event.currentTarget
let hasAttr = target.hasAttribute(AttrToggle)
let elmAccordion
// アコーディオンする要素を取得して、`elmAccordion`に格納
target.childNodes.forEach(childItem => {
if (childItem.className == ClassAccordionElm) {
elmAccordion = childItem
}
})
// `toggle_attr`の属性の有無でアコーディオンを「開く」か「閉じる」か、どちらかの処理を実行
if (hasAttr) {
target.removeAttribute(AttrToggle)
elmAccordion.style.height = '0'
} else {
target.setAttribute(AttrToggle, '')
elmAccordion.style.height = `${arryAccordionElmHeight[index]}px`
}
}) // / addEventListener
}) // / forEach
}
// ここで関数を呼び出し
Accordion({
wrap_id: 'first-accordion',
item_class: 'p-accordion__item',
accordion_class: 'p-accordion__content',
accordion_transition: '.4s',
toggle_attr: 'data-active',
default_is_open: true,
})
HTML
<ul class="p-accordion" id="first-accordion">
<li class="p-accordion__item">
<p class="p-accordion__head">
<a href="javascript:void(0);">1つ目</a>
</p>
<div class="p-accordion__content">
<p>text</p><p>text</p><p>text</p>
</div>
</li>
<li class="p-accordion__item">
<p class="p-accordion__head">
<a href="javascript:void(0);">2つ目</a>
</p>
<div class="p-accordion__content">
<p>text</p><p>text</p><p>text</p>
<p>text</p><p>text</p><p>text</p>
</div>
</li>
<li class="p-accordion__item">
<p class="p-accordion__head">
<a href="javascript:void(0);">3つ目</a>
</p>
<div class="p-accordion__content">
<p>text</p><p>text</p><p>text</p>
<p>text</p><p>text</p><p>text</p>
<p>text</p><p>text</p><p>text</p>
</div>
</li>
</ul>
CSS
*{
margin: 0;
padding: 0;
box-sizing: border-box;
}
li{
list-style: none;
}
#wrap{
width: 900px;
padding: 40px 40px;
margin: auto;
}
.p-accordion{
display: flex;
justify-content: space-between;
}
.p-accordion__item{
width: 32%;
}
.p-accordion__head a{
display: block;
color: #fff;
text-decoration: none;
background-color: #333;
padding: 8px 12px;
position: relative;
}
.p-accordion__head a::before,
.p-accordion__head a::after{
content: "";
display: block;
width: 16px;
height: 2px;
background-color: #fff;
position: absolute;
top: 0;
bottom: 0;
right: 12px;
margin: auto 0;
transition: .2s;
}
.p-accordion__head a::before{
transform: rotate(90deg);
}
.p-accordion__item[data-active] .p-accordion__head a::before{
transform: rotate(270deg);
opacity: 0;
}
.p-accordion__item[data-active] .p-accordion__head a::after{
transform: rotate(180deg);
}
.p-accordion__content{
margin-top: 8px;
padding: 0 12px;
}
使い方
Accordion({
wrap_id: 'first-accordion',
item_class: 'p-accordion__item',
accordion_class: 'p-accordion__content',
accordion_transition: '.4s',
toggle_attr: 'data-active',
default_is_open: true,
})
Accordion({
キー:値,
キー:値,
キー:値,
})
キー | 値 |
wrap_id | アコーディオンを囲っている親要素のID |
item_class | アコーディオンが複数ある場合、その繰り返されている要素のclass |
accordion_class | 実際に表示非表示が切り替わる要素のclass |
accordion_transition | 表示非表示が切り替わる際のアニーションのスピード デフォルト:'.4s' |
toggle_attr | アコーディオンする要素が表示の時に、item_classに付与される属性名 デフォルト:'data-active' |
default_is_open | ページ読み込み時に、アコーディオンする要素を表示の状態にするかどうか デフォルト:false(非表示) |
注意点
wrap_id、item_class、accordion_classで値を設定する際、それぞれ別の要素としてIDとclassを設定して下さい。
<!-- OK -->
<div id="wrap_id">
<div class="item_class">
<p class="accordion_class">text</p>
</div>
</div>
<!-- NG -->
<!-- wrap_id と item_class が同じ要素に設定されているからNG -->
<div id="wrap_id" class="item_class">
<p class="accordion_class">text</p>
</div>
簡単な解説
今回のサンプルコードは、ES6の「テンプレート文字列」の書き方を使っているので、コンパイル環境が無い場合は適所書き換えて下さい。
変数と文字列を同時を結合する場合、これまでは「+」を使って組み合わせていましたが、「テンプレート文字列」の書き方をすることで、「+」を使わなくても変数と文字列を結合出来ます。
`#${obj.wrap_id} .${obj.item_class}`
↓
'#' + obj.wrap_id + ' .' + obj.item_class
関数呼び出し時の引数をもとに、要素などを取得します。
アコーディオンさせる要素の高さを取得して、js側で高さの切り替えを行なっています。
なので、アコーディオンさせる要素が複数あっても、それぞれCSSでheightを設定しなくて大丈夫です。
CSSでheightのアニメーションを作る場合、「0pxから100px」のような値の変わり方なら、transitionで滑らかな切り替わりが表現出来ます。
しかし、「autoから100px」のような値の変わり方の場合、transitionを使用しても動きは滑らかになりません。
アコーディオンさせる要素が表示の時は、toggle_attrで設定した属性を付与して、非表示の時はその属性は削除されます。
なので、表示、非表示でアイコンの切り替え(プラスとマイナスなど)などを行いたい場合は、「toggle_attrで設定した属性が付いている時は○○」のような書き方をすればOKです。
まとめ
今回のサンプルコードには、コメントアウトで出来るだけ処理の説明を残しておきましたので、カスタマイズする際の参考にして頂ければと思います。
このような汎用性の高いサンプルコードをこれからは作っていこうと思うので、その時はまた本ブログにてご紹介させて頂きます。