🎉

Framer Motionでアコーディオンアニメーション実装すると楽

2022/08/19に公開約3,200字

以前、Framer Motion を試してみた記事を書いたのですが、その続編で、アコーディオンのアニメーションを実装してみました。高さが変わってアコーディオンが開くアニメーションです。

実施環境
項目 詳細
PC MacBook Pro(14 インチ、2021)Apple M1 Pro
OS MacOS Monterey 12.5
Node.js v16.14.2
Next.js v12.1.0
React.js v17.0.1
Framer Motion v7.0.0

Framer Motion を使わずに実装

開いたあとの高さがわかっている場合

高さがわかっている場合のアニメーションは、CSS(一部クラス切り替えのみ js)で簡単に実装できます。アニメーション開始前、終了後の高さを指定してあげて、transition を記述すれば OK です。

高さがわかっているアコーディオンアニメーション
.accordion {
  /* 最初の高さ */
  height: 50px;
  transition: 1s;
  ...

  &.-open {
    /* 開いたあとの高さ */
    height: 130px;
  }
}

開いたあとの高さがわかっていない場合

ただ、前後の高さが固定のケースは実際あまりなく、height は auto 等を指定したいかと思います。が、アニメーション終了後を auto 等にすると transition が効きません。

高さがわかっていないアコーディオンアニメーション(動かない)
.accordion {
  /* 最初の高さ */
  height: 50px;
  transition: 1s;
  ...

  &.-open {
    /* 開いたあとの高さ */
    height: auto;
  }
}

それに対する CSS での解法としては、max-height を使うなどがあるようです。

https://qiita.com/irico/items/47d9086793e13b72817d

高さがわかっていないアコーディオンアニメーション(動くが…)
.accordion {
  /* 最初の高さ */
  max-height: 50px;
  transition: 1s;
  ...

  &.-open {
    /* 開いたあとの高さ */
    max-height: 100vh;
  }
}

ただしこの場合、transition 時間を例えば 1s とした場合、アニメーションは 1s で 100vh になるように実行されます。そのため、サンプルでもわかるように、開く時は 1s にしては早すぎ、閉じる時は最初の数ミリ秒アニメーションが開始されません。あと、高さが 100vh を超える場合は 100vh までしか開きません。100vh ではなく 9999px とかありえないほど大きい高さを指定する方法も考えられますが、その場合前述のアニメーション時間のバグがひどくなります。

そのため、そのへんも考慮した場合は js 等で予め高さを取得してあげるしか無い気がします。

Framer Motion

Framer Motion を使えば、100%や auto などを設定しても、よしなにアニメーションしてくれます。

公式のサンプル

https://codesandbox.io/s/framer-motion-accordion-qx958?file=/src/Example.tsx:743-1048

実装例

公式だと表示、非表示の出し分けだったので、上記 codepen と同じようなタイプの実装をしてみました。

https://nextjs-671qth-iq3zu1hp--3000.local.webcontainer.io/accordion

実装

Framer Motionでのアコーディオン実装
import React, { useState } from 'react';
import styles from './index.module.css';
import { motion, useAnimationControls } from 'framer-motion';

const TestPageAccordion = () => {
  const [isExpanded, setIsExpanded] = useState(false);

  const toggleExpanded = () => {
    if (isExpanded) {
      controls.start({
        height: '50px',
      });
    } else {
      controls.start({
        height: '100%',
      });
    }

    setIsExpanded(!isExpanded);
  };

  const controls = useAnimationControls();

  return (
    <div className={styles['main-contents']}>
      <h1>Accordion</h1>
      <button onClick={toggleExpanded}>open</button>
      <motion.div
        className={styles.accordion}
        animate={controls}
        transition={{ duration: 1 }}
      >
        aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
      </motion.div>
    </div>
  );
};

export default TestPageAccordion;

まとめ

アコーディオンのアニメーションって結構難しいと感じてたのですが、Framer Motion を使えばかなり楽に実装できました。ぜひ試してみてください。

Discussion

ログインするとコメントできます