Framer-motionを使ったアニメーション

2024.07.30

アニメーションライブラリ「Framer Motion」を使ってみました。

まだ使い込んでいないですが、ページ遷移時や要素がブラウザ画面に入った時など様々なタイミングでアニメーションを使えるようです。今まではcssで一つ一つ作っていましたがシンプルで使い勝手が良さそうなのでReactでは積極的に使っていこうと思います。

実装時はこちらのサイトなどを参考にさせていただきました。
ReactでFramer Motionを使ってアニメーション

基本的な使い方

まずはインストール。

npm install framer-motion

次にコードです。motionタグでアニメーションしたい要素を囲うことで実装できます。「motion.div」という感じでドットで使いたいタグを指定も必要です。

import React from 'react';
import { motion } from 'framer-motion';

const MyComponent = () => {
  return (
    <motion.div
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      transition={{ duration: 2,delay: 1 }}
    >
      Hello, Framer Motion!
    </motion.div>
  );
};

export default MyComponent;

motion.divタグの中にアニメーションに関係するプロパティを記載します。
「initial」がアニメーション開始時
「animate」が完了時
「transition」duration: 2はアニメーションを何秒かけて実行するかです。
「transition」delay: 1はアニメーションを開始する時間を変えています。

このサンプルはページが表示されて1秒遅れた後に2秒間かけて透過していた要素を表示するアニメーションです。サンプルはopacityになっていますが、rotateでもscaleでも他のCSSプロパティも使えます。書き方もCSSと似ており使いやすそうですね。

CSSプロパティをタグに直接書きたくない場合

先のコードはCSSプロパティが一つなので視認性は気になりませんが、もっと複雑なアニメーションの場合はCSSプロパティが増え読みづらくなってしまいます。

その場合はvariantsを使うとプロパティをまとめることができます。

import React from 'react';
import { motion } from 'framer-motion';

const fadeInVariants = {
  hidden: { opacity: 0, scale: 0.8 },
  visible: { opacity: 1, scale: 1 },
};

const MyComponent = () => {
  return (
    <motion.div
      initial="hidden"
      animate="visible"
      variants={fadeInVariants}
      transition={{ duration: 1 }}
    >
      Hello, Framer Motion!
    </motion.div>
  );
};

export default MyComponent;

これでOKです!fadeInVariantsオブジェクトに「hidden」と「visible」を使用していますが、任意の名前を使って大丈夫です。

真偽値でわけてアニメーションを使い分けることも可能

条件を満たした時だけアニメーションを実装したい場合もあります。そんなときは真偽値を使って判定することもできます。

import React from 'react';
import { motion } from 'framer-motion';

// boxVariantsはJavaScriptのオブジェクト
const boxVariants = {
  hidden: { opacity: 0, scale: 0.8 },
  visible: { opacity: 1, scale: 1 },
};

const AnimatedBox = ({ isVisible }) => {
  return (
    <motion.div
      initial="hidden"
      animate={isVisible ? "visible" : "hidden"}
      variants={boxVariants}
      transition={{ duration: 0.5 }}
    >
      Animate me!
    </motion.div>
  );
};

export default AnimatedBox;

こちらのように「isVisible」がtrueのとき「animate」で設定したアニメーションを実行することができます。

親要素のアニメーションが終わってから子要素のアニメーションを実行したい

tansition:delayで一つづ親要素と子要素のアニメーションタイミングを調整することもできますが、面倒という場合もあります。そんな時はこちら。

when: ‘beforeChildren’により子要素アニメーションの前に実行できます。子要素のアニメーションは親要素の後に行われます。

import React from 'react';
import { motion } from 'framer-motion';

// 親要素のアニメーションのVariants
const parentVariants = {
  hidden: { opacity: 0, scale: 0.8 },
  visible: { 
    opacity: 1, 
    scale: 1,
    transition: {
      duration: 1,
      when: 'beforeChildren', // 子要素のアニメーションの前に実行
      staggerChildren: 0.5,   // 子要素のアニメーションを0.5秒ずつずらす
    }
  },
};

// 子要素のアニメーションのVariants
const childVariants = {
  hidden: { opacity: 0, y: 50 },
  visible: { 
    opacity: 1, 
    y: 0,
    transition: {
      duration: 1
    }
  },
};

const ParentChildAnimation = () => {
  return (
    <motion.div
      initial="hidden"
      animate="visible"
      variants={parentVariants}
    >
      <motion.div variants={childVariants}>
        Child Animation 1
      </motion.div>
      <motion.div variants={childVariants}>
        Child Animation 2
      </motion.div>
      <motion.div variants={childVariants}>
        Child Animation 3
      </motion.div>
      Parent Animation
    </motion.div>
  );
};

export default ParentChildAnimation;

上記のサンプルでは「staggerChildren」を使っています。これは子要素を順番にアニメーションさせたい時に使用。サンプル例では0.5秒ごとに子要素が一つづつ実行されます。この指定がない場合は複数の子要素のアニメーションは一斉に行われます。

PCとスマホでアニメーションを分けたい

例えばメインビジュアルなどの場合です。
スマホが縦長でPCが横長なため、CSSプロパティを分けたい場合があります。そんな時はjsで判定をして真偽値で対応するvariantsを分けてあげましょう。

import React, { useEffect, useState } from 'react';
import { motion } from 'framer-motion';

// PC用のアニメーションのVariants
const pcVariants = {
  hidden: { opacity: 0, scale: 0.8 },
  visible: { 
    opacity: 1, 
    scale: 1,
    transition: {
      duration: 1,
    }
  },
};

// モバイル用のアニメーションのVariants
const mobileVariants = {
  hidden: { opacity: 0, x: -100 },
  visible: { 
    opacity: 1, 
    x: 0,
    transition: {
      duration: 1,
    }
  },
};

const ParentAnimation = () => {
  const [isMobile, setIsMobile] = useState(false);

  useEffect(() => {
    const checkDevice = () => {
      const isMobileDevice = window.innerWidth <= 768;
      setIsMobile(isMobileDevice);

      // デバイスの種類をコンソールに表示
      if (isMobileDevice) {
        console.log("Mobile device detected");
      } else {
        console.log("PC device detected");
      }
    };

    // 初期チェック
    checkDevice();

    // リサイズ時にデバイスの種類をチェック
    window.addEventListener('resize', checkDevice);

    return () => {
      window.removeEventListener('resize', checkDevice);
    };
  }, []);

  return (
    <motion.div
      initial="hidden"
      animate="visible"
      variants={isMobile ? mobileVariants : pcVariants}
    >
      Parent Animation
    </motion.div>
  );
};

export default ParentAnimation;

useEffectを使って画面サイズの状態を管理し、isMobileでモバイルとPCのVariantsを分けています。

モバイル判定はreact-responsiveというライブラリを使えばもっと短いコードになります。
…が、いろいろ調べるとこのライブラリは表示時にちらつきが発生する可能性があるようでした。なので今回はJavaScriptでコードを書いてモバイル判定をするようにしています。
コードは長くなりますが、チラつくくらいならこっちの方がいいかなと…

アニメーションが終わった後に何か処理をしたい

アニメーションが終わった後に別の処理をしたい場合があります。

その時はFramer-motionのonAnimationCompleteを使うと便利です。

import React from 'react';
import { motion } from 'framer-motion';

// アニメーションのVariants
const animationVariants = {
  hidden: { opacity: 0, scale: 0.8 },
  visible: { 
    opacity: 1, 
    scale: 1,
    transition: {
      duration: 1,
    }
  },
};

const SimpleAnimation = () => {
  const handleAnimationComplete = () => {
    console.log("Animation completed");
  };

  return (
    <motion.div
      initial="hidden"
      animate="visible"
      variants={animationVariants}
      onAnimationComplete={handleAnimationComplete}
    >
      Animation Content
    </motion.div>
  );
};

export default SimpleAnimation;

onAnimationComplete={handleAnimationComplete}、こちらでアニメーションが終わった後の処理を指定しています。これだけでOKです!

一度だけアニメーションを実行したい場合

メインビジュアルなど長めのアニメーションありますよね。サイトの印象を伝える大事なものです。

初めてユーザーがサイトを訪れた時はよいですが、下層ページからトップページに戻ってきた時に再び長めのアニメーションが発生するとユーザーにとってはしつこい感じがします。サイト離脱につながりかねません。

Recoilを使って状態管理をするとサイトに訪れた一度だけアニメーションを実行することができます。
※補足ですが、ブラウザをリロードしたりサイトから離れると状態管理は初期に戻るためアニメーションは実行されます。

import { motion } from 'framer-motion';
import { useRecoilState } from 'recoil';
import { firstTimeStateFamily } from '../../store/atomFirstTimeState.ts';
import './styles.scss';

const mvVariants = {
  visible: {
    opacity: 1,
    scale: 1,
    transition: {
      when: 'beforeChildren',
      duration: 3,
      staggerChildren: 1,
    },
  },
  first: {
    opacity: 0,
    scale: 1.2,
  },
};

const textVariants = {
  visible: { opacity: 1, transition: { duration: 1 } },
  first: { opacity: 0 },
};

const Mv = () => {
  const [isParentFirstTime, setIsParentFirstTime] = useRecoilState(firstTimeStateFamily('parent'));
  const [isChildFirstTime, setIsChildFirstTime] = useRecoilState(firstTimeStateFamily('child'));

  const handleParentAnimationComplete = () => {
    if (isParentFirstTime) {
      setIsParentFirstTime(false);
    }
  };

  const handleChildAnimationComplete = () => {
    if (isChildFirstTime) {
      setIsChildFirstTime(false);
    }
  };

  return (
    <motion.div
      variants={isParentFirstTime ? mvVariants : {}}
      animate="visible"
      initial="first"
      onAnimationComplete={handleParentAnimationComplete}
      style={{
        width: '100vw',
        height: '100vh',
        backgroundColor: 'gray',
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
      }}
    >
      <motion.p
        variants={isChildFirstTime ? textVariants : { visible: { opacity: 1 } }}
        onAnimationComplete={handleChildAnimationComplete}
      >
        子要素のアニメーションテキスト
      </motion.p>
    </motion.div>
  );
};

export default Mv;

サンプルではメインビジュアルがあり、その子要素にテキストがある想定です。
インラインでstyleを指定している箇所は画面いっぱいに表示するためのスタイルになります。

次にAtomです。親要素と子要素のアニメーションの実行状態をatomFamilyを使ってそれぞれ管理します。

import { atomFamily } from 'recoil';

export const firstTimeStateFamily = atomFamily({
  key: 'firstTimeStateFamily',
  default: true,
});

アニメーションが一度実行されたかどうかは、variantsの真偽値で判定。こちらの部分ですね。

variants={isParentFirstTime ? mvVariants : {}}

そしてonAnimationCompleteを使って関数を実行。atomにアニメーションが実行されたことを伝えます。
親要素と子要素それぞれ分けて状態管理をしています。

下記はatomFamilyに使う処理。親要素と子要素状態をfalseにすることで2回目以降アニメーションを実行しないようにします。

  const [isParentFirstTime, setIsParentFirstTime] = useRecoilState(firstTimeStateFamily('parent'));
  const [isChildFirstTime, setIsChildFirstTime] = useRecoilState(firstTimeStateFamily('child'));

  const handleParentAnimationComplete = () => {
    if (isParentFirstTime) {
      setIsParentFirstTime(false);
    }
  };

  const handleChildAnimationComplete = () => {
    if (isChildFirstTime) {
      setIsChildFirstTime(false);
    }
  };

これで別ページからアニメーションのあるページに戻った時はアニメーションは実行されません。
完成!

…と、言いたいところですが注意点。
子要素のアニメーションでは明示的に完了時のCSSプロパティを記述しています。
visible: { opacity: 1 }の部分です。

<motion.p
  variants={isChildFirstTime ? textVariants : { visible: { opacity: 1 } }}
  onAnimationComplete={handleChildAnimationComplete}
>

上記の理由は明示的に書かないとアニメーション開始時に戻ってしまうためです。

この原因が掴めてないですが、Framer-motionのレンダリングのタイミングの影響なのかもしれません…。
サンプルの例だと、明示的に書かないと透過→表示のあとに再びアニメーション開始時の透過の状態に戻ってしまうのです。

他にいい方法があるかもしれませんが、現状はこれで対応しています。

まとめ

Framer-motion、使ってみましたがかなり便利だと思いました。マウスイベントや画面に要素が入った際なども指定できるようなので今後も使っていきたいと思います。

いい機能があれば随時、追記していきます!

totop Page Top