CSSアニメーションで高さ可変のアコーディオン

jQueryを使わず、cssアニメーションで高さ可変のアコーディオンを実装した際の、試行錯誤の記録です。

## jQueryの場合
jQueryなら、アコーディオンメニューも簡単に実装できます。

```html:accordion.html
<ul>
  <li>
    <button class="js-button">ボタン</button>
    <div class="accordion js-accordion">
      <p>アコーディオンするコンテンツ</p>
    </div>
  </li>
  <li>
    <button class="js-button">ボタン2</button>
    <div class="accordion js-accordion">
      <p>アコーディオンするコンテンツ2アコーディオンするコンテンツ2</p>
    </div>
  </li>
</ul>
```

```javascript:jquery-accordion.js
$('.js-button').on('click', function() {
  $(this).next('.js-accordion').slideToggle();
});
```

## cssアニメーション
jQueryなしで実装する場合は、cssアニメーションを使います。
パフォーマンス的にも、jQueryのアニメーションではなくcssのアニメーションを使用した方が良いです。
（なので、jQueryは開閉のトリガーだけに使用し、アニメーションはcssで行う方法もあります。）

ただ、アコーディオンするコンテンツの高さが可変の場合、ちょっと面倒です。。

まずは、開閉のトリガーのみをjsで設定します。

```javascript:trigger.js
const button = document.querySelectorAll('.js-button');
const openClass = 'is-expand';
Array.prototype.forEach.call(button, (el) => {
  const contents = el.nextElementSibling;
  el.addEventListener('click', (e) => {
    e.preventDefault();
    el.classList.toggle(openClass);
    contents.classList.toggle(openClass);
  });
});
```

### heightをアニメーションする
`height: 0;`から`height: auto;`だと、アニメーションしてくれない。

```css:height-auto.css
.accordion {
  height: 0;
  transition: height 0.3s linear;
}
.accordion.is-expand {
  height: auto;
}
```

### max-heightをアニメーションする
`max-height: auto;`もアニメーションしてくれないので、十分に大きな数値を設定してあげて対応すれば動く。

```css:max-height-auto.css
.accordion {
  max-height: 0;
  transition: max-height 0.3s linear;
}
.accordion.is-expand {
  max-height: 9999px;
}
```

#### JavaScriptでmax-heightを取得
ただその`9999px`というマジックナンバーには明確な意味がないので、JavaScriptで中身の高さを取得して設定するようにしてみました。

```javascript:set-max-height.js
const button = document.querySelectorAll('.js-button');
const openClass = 'is-expand';
Array.prototype.forEach.call(button, (el) => {
  el.addEventListener('click', (e) => {
    e.preventDefault();
    const contents = el.nextElementSibling;
    const outerHeight = contents.clientHeight;
    // 子要素の高さを取得
    const innerHeight = contents.children[0].clientHeight;

    el.classList.toggle(openClass);
    contents.classList.toggle(openClass);

    // 子要素の高さを親要素のmax-heightに設定
    contents.style.maxHeight = outerHeight === 0 ? `${innerHeight}px` : 0;
  });
});
```
```css:set-max-height.css
.accordion {
  /* 子要素の高さは維持しながら、.accordionを見えなくする */
  max-height: 0;
  min-height: 0;
  overflow-y: hidden;
  transition: all .5s ease-in-out;
}
.accordion.is-expand {
  transition: all .2s ease-in-out;
}
```

【ポイント】
子要素`.accordion > p`の高さは維持しながら、`.accordion`を非表示にすることで、`max-height`に指定するコンテンツの高さを取得することができます。


しかし、（スマホのデバイスを回転させるなど）画面がリサイズされるとアコーディオンパネルの高さが変わり、コンテンツが見切れてしまう場合が。。
resize時に`max-height`を設定し直すように修正すれば良いが、どんどん複雑になっていく・・・。


### max-heightを使わずにアニメーション
max-heightを使わずに、それっぽく見せるには・・・？。
先人も、試行錯誤している模様・・・参考にさせていただきました。
https://stackoverflow.com/questions/3508605/how-can-i-transition-height-0-to-height-auto-using-css

#### margin-top: -100%;（transform: translateY(-100%);）を使う
中身が上から下りてくる感じになる（スライダー的な動き）
参考：
https://codepen.io/diegopardo/full/pvkjx

```css:margin.css
.accordion {
  margin-top: -100%;
  /* transform: translateY(-100%); でも同じ */
  transition: all .2s ease-out;
}
.accordion > p {
  opacity: 0;
}
.accordion.is-expand {
  margin-top: 0;
 /* transform: translateY(0); */
}
.accordion.is-expand > p {
  opacity: 1;
}
```

#### transform: scaleY(0)を使う
変形してしまうのが許容できるなら。。

```css:transform.css
.accordion {
  transform: scaleY(0);
  transition: all .2s ease-out;
}
.accordion.is-expand {
  transform: scaleY(1);
}
```

#### height以外の要素でそれっぽく見せる
`height:auto;`でなぜかアニメーションしているものがある！？
調べてみると、`transition-property: all;`で`height`以外の要素がアニメーションすることで、それっぽく見えているようでした。
`padding`, `margin`, `line-height` etc...

```css:not-height.css
.accordion {
  height: 0;
  margin-top: 0;
  overflow: hidden;
  opacity: 0;
  padding: 0;
  transition: all .2s ease-out;
}
.accordion.is-expand {
  height: auto;
  margin-top: 16px;
  opacity: 1;
  padding: 16px 5px 0 20px;
  transition: all .2s ease-out;
}
```

#### 子要素を、高さ以外でアニメーションさせる
子要素が単純なもの（`line-height`のみで高さが変更可能）の場合は良さそう。

```css:child.css
.accordion > p {
  line-height: 0;
  opacity: 0;
  padding: 0 10px;
  transition: all .2s ease-out;
}
.accordion.is-expand > p {
  line-height: 1.25;
  opacity: 1;
  padding: 16px 10px;
}
```
サンプル：
https://codepen.io/kzmhrd/pen/zYObKmr

#### 子要素の位置をずらして、それっぽく見せる
参考：
https://codepen.io/LasyAsCat/pen/KdxLRj

```html:translateY.html
<ul>
  <li>
    <button class="js-button">ボタン</button>
    <div class="accordion js-accordion">
      <div class="accordion__contents">
        <div class="accordion__inner">アコーディオンするコンテンツ</div>
      </div>
    </div>
  </li>
  <li>
    <button class="js-button">ボタン2</button>
    <div class="accordion js-accordion">
      <div class="accordion__contents">
        <div class="accordion__inner">アコーディオンするコンテンツ2アコーディオンするコンテンツ2</div>
      </div>
    </div>
  </li>
</ul>
```

```css:translateY.css
/* 外側の枠の高さを設定 */
.accordion {
  max-height: 0;
  overflow: hidden;
  transition: max-height .3s linear;
}
.accordion.is-expand {
  max-height: 100%;
  transition: max-height .4s linear;
}

/* コンテンツを上下に動かしておいて、見えない状態にしておく */
/* コンテンツを上に */
.accordion__contents {
  overflow: hidden;
  transform: translateY(-100%);
  transition: transform .3s linear;
}
/* コンテンツを下に */
.accordion__inner {
  overflow: hidden;
  transform: translateY(100%);
  transition: transform .3s linear;
}

/* コンテンツの位置を0にして表示させる */
.accordion.is-expand .accordion__contents {
  transform: translateY(0);
  transition: transform .2s linear;
}
.accordion.is-expand .accordion__inner {
  transform: translateY(0);
  transition: transform .2s linear;
}
```
サンプル：
https://codepen.io/kzmhrd/pen/pozYNvo

`.accordion`の高さが固定されていればいい感じですが、高さを変化させるために、結局、`max-height`との合わせ技になってしまったため、ちょっと動きが微妙。。
今回は、アコーディオン内の要素が複雑だったため、これを採用しました。

## まとめ
cssアニメーションには正解がないことが分かりました。。
アイデア一つでいろいろできるので、jsで複雑な処理を書くくらいなら、いろいろ試してみると良いと思いました。