Framer MotionはReactで利用することができるシンプルなアニメーションライブラリです。Reactで構築したアプリケーションにアニメーションをつけたい場合におすすめのライブラリです。

本文書では初めてFramer Motionを利用する人向けの内容になっているので複雑なコードは利用しておらずシンプルなコードで動作確認を行っています。

環境の構築

アニメーションの動作は実際に動作確認することで簡単に理解できるのでViteを利用してReactプロジェクトを作成して設定を行なっていきます。

プロジェクトの作成

npm create vite@latest コマンドで React のプロジェクトを作成します。npm create vite@latest コマンドを実行するとプロジェクト名の入力とフレームワークの選択、TypeScript を利用するかどうかの選択する必要があります。プロジェクト名には任意の名前をつけることができるので”react-framer-motion”という名前を指定し、フレームワークでは React を選択してください。本文書では TypeScript は利用しないので JavaScript を選択しています。


% npm create vite@latest

framer-motionのインストール

プロジェクト作成完了後に作成されたプロジェクトフォルダに移動して npm install を実行後、framer-motion ライブラリのインストールを行います。


% npm install
% npm install framer-motion

バージョンが異なると設定方法が変わる可能性もあるので framer-motion のインストール後に今回動作確認を行った各種ライブラリのバージョンを package.json ファイルで確認しておきます。


{
  "name": "react-framer-motion",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
    "preview": "vite preview"
  },
  "dependencies": {
    "framer-motion": "^10.16.4",
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@types/react": "^18.2.15",
    "@types/react-dom": "^18.2.7",
    "@vitejs/plugin-react": "^4.0.3",
    "eslint": "^8.45.0",
    "eslint-plugin-react": "^7.32.2",
    "eslint-plugin-react-hooks": "^4.6.0",
    "eslint-plugin-react-refresh": "^0.4.3",
    "vite": "^4.4.5"
  }
}

環境の構築とバージョン確認後に画面上に Vite のロゴのみ表示するように App.jsx ファイルを更新します。


import viteLogo from '/vite.svg';
import './App.css';

function App() {
  return (
    <div className="App">
      <div>
        <a href="https://vitejs.dev" target="_blank">
          <img src={viteLogo} className="logo" alt="Vite logo" />
        </a>
      </div>
    </div>
  );
}

export default App;

npm run devコマンドを実行すると画面にはViteのロゴのみ表示されます。

ロゴが画面上に表示
ロゴが画面上に表示

動作確認

回転(rotate)

インストールした framer-motion を利用してロゴを 135 度回転させます。回転を行いたい要素のタグを div から motion.div タグに変更して animate props を追加しプロパティと値のペアを持つオブジェクトを設定します。回転の場合は rotate プロパティを利用してその値を 135 に設定します。これで framer-motion を利用した rotate の設定は完了です。


import viteLogo from '/vite.svg';
import './App.css';
import { motion } from 'framer-motion';

function App() {
  return (
    <div className="App">
      <motion.div animate={{ rotate: 135 }}>
        <a href="https://vitejs.dev" target="_blank">
          <img src={viteLogo} className="logo" alt="Vite logo" />
        </a>
      </motion.div>
    </div>
  );
}

export default App;

設定後にページをリロードすると画面が表示された瞬間にアニメーションで 135 度回転して止まります。表示した時から 135 度回転した状態で表示されるのではなくアニメーションで 135 度回転を行います。

回転後のロゴ
回転後のロゴ

transition(duration)

ゆっくりと回転させたい場合には transition props を追加して duration プロパティに値 3 を設定します。3 は 3 秒を意味しているので animate props のみを設定した時とは異なり、3 秒かけてゆっくりと回転します。


<motion.div animate={{ rotate: 135 }} transition={{ duration: 3 }}>
  <a href="https://vitejs.dev" target="_blank">
    <img src={viteLogo} className="logo" alt="Vite logo" />
  </a>
</motion.div>

transiton(delay)

ページをリロードするとすぐにアニメーションが開始されますが、アニメーションを開始するまでの時間を変えたい場合には transition の delay を利用することができます。


<motion.div animate={{ rotate: 135 }} transition={{ delay: 3, duration: 3 }}>
  <a href="https://vitejs.dev" target="_blank">
    <img src={viteLogo} className="logo" alt="Vite logo" />
  </a>
</motion.div>

上記の設定ではページを開いてから 3 秒後にアニメーションが開始され、3 秒かけてアニメーションが回転します。

初期値の設定

アニメーションの開始時の設定を initial props を利用して設定することができます。opacity が 0 から開始し、最終的に opacity を 1 にしたい場合には下記のように記述することができます。終了時の設定は animate props で行っています。5 秒かけてゆっくり回転を行いながら段々とロゴが表示されてきます。


<motion.div
  animate={{ opacity: 1, rotate: 135 }}
  transition={{ duration: 5 }}
  initial={{ opacity: 0 }}
>
  <a href="https://vitejs.dev" target="_blank">
    <img src={viteLogo} className="logo" alt="Vite logo" />
  </a>
</motion.div>

要素の移動

要素の移動を行いたい場合には x, y を利用することができます。initial props で x は-200px、animate props で x を 200px に設定しているので x が-200px の位置から開始して最終的に animate props の設定で x は 200px の位置でアニメーションが終了します。


<motion.div
  animate={{ x: '200px', rotate: 135 }}
  transition={{ duration: 5 }}
  initial={{ x: '-200px' }}
>
  <a href="https://vitejs.dev" target="_blank">
    <img src={viteLogo} className="logo" alt="Vite logo" />
  </a>
</motion.div>

initial props のみ設定した場合には initial props で設定した状態で表示されます。下記では元の場所よりも-200px 左側の位置で表示されます。


<motion.div initial={{ x: '-200px' }}>
  <a href="https://vitejs.dev" target="_blank">
    <img src={viteLogo} className="logo" alt="Vite logo" />
  </a>
</motion.div>

アニメーション時の変化

framer motion を利用することでアニメーションを実装できることがわかりました。では実際にどのようにアニメーションが行われているか確認するためにデベロッパーツールを利用して要素を確認してみます。コードは先ほどの要素の移動を利用します。


<motion.div
  animate={{ x: '200px', rotate: 135 }}
  transition={{ duration: 5 }}
  initial={{ x: '-200px' }}
>
  <a href="https://vitejs.dev" target="_blank">
    <img src={viteLogo} className="logo" alt="Vite logo" />
  </a>
</motion.div>

アニメーションを開始してしばらくしてからの要素の状態です。style 属性の transform を利用して transleteX と rotate が変化していることが確認できます。

アニメーションの途中の状態
アニメーションの途中の状態

最終的にはanimate propsで設定した値(x:200px, rotate:135)になります。

アニメーションの最終的な値
アニメーションの最終的な値

もし framer motion を利用せずに同じようなアニメーションを設定したい場合は下記のように行うことができます。App.css ファイルで initianl と animate を設定します。


.initial {
  transform: translateX(-200px) rotate(0deg);
  transition: transform 5s;
}

.animate {
  transform: translateX(200px) rotate(135deg);
}

画像をクリックしたらアニメーションを開始するように設定を行っています。


import viteLogo from '/vite.svg';
import './App.css';
import { useState } from 'react';

function App() {
  const [active, setActive] = useState(false);
  return (
    <div className="App">
      <div
        className={`initial ${active ? 'animate' : ''}`}
        onClick={() => setActive(true)}
      >
        <img src={viteLogo} className="logo" alt="Vite logo" />
      </div>
    </div>
  );
}

export default App;

全く同じアニメーションではありませんが画像をクリックすると移動しながら回転します。

Spring(バネの動き)の設定

バネの動きを利用したアニメーションを設定した場合は animate props の type を spring に設定することで実現できます。下記の設定では上からロゴが落ちてきて少しだけバウンスをします。


<motion.div
  animate={{ y: 0, rotate: 135 }}
  transition={{ type: 'spring' }}
  initial={{ y: '-50vh' }}
>
  <a href="https://vitejs.dev" target="_blank">
    <img src={viteLogo} className="logo" alt="Vite logo" />
  </a>
</motion.div>

type プロパティに spring を設定した場合に、bounce, stiffness, damping などのプロパティを設定することでバネの動きを変えることができます。


transition={{ type: 'spring', stiffness: 100 }}

Hoverの設定

ロゴの上にマウスのポインターをHoverさせた時にアニメーションを行いたい場合はwhileHover propsを利用します。


<motion.div
  whileHover={{
    scale: 1.2,
    transition: { duration: 1 },
  }}
>
  <a href="https://vitejs.dev" target="_blank">
    <img src={viteLogo} className="logo" alt="Vite logo" />
  </a>
</motion.div>

ロゴの上にマウスのポインターをあてるとdurationの1秒かけてゆっくりとロゴが拡大(1.2倍)します。

Hoverイベントの設定

マウスのポインターをhoverした時にイベントを発火させることもできます。


<motion.div
  whileHover={{
    scale: 1.5,
    transition: { duration: 1 },
  }}
  onHoverStart={(e) => {
    console.log('start');
  }}
  onHoverEnd={(e) => {
    console.log('end');
  }}
>
  <a href="https://vitejs.dev" target="_blank">
    <img src={viteLogo} className="logo" alt="Vite logo" />
  </a>
</motion.div>

マウスポインターをhoverするとコンソールには”start”が表示され、ポインターを要素から外すと”end”が表示されます。

onHoverStartの関数の引数にはevent(MouseEvent)とinfoを指定することができます。infoにはマウスポインターのx, yの値が保存されてます。


onHoverStart={(e, info) => {
  console.log('start');
  console.log(e);
  console.log(info);
}}
//info
point:
 x: 576.8853759765625
 y: 480.32666015625

Hover以外にもFocus, Tap, Panなどがあります。

Dragの設定

要素を Drag したい場合には drag props を設定するだけです。画像だけでは drag できないので button 要素を追加します。


<motion.div
  drag
  whileHover={{
    scale: 1.5,
    transition: { duration: 1 },
  }}
>
  <button>Drag</button>
  <a href="https://vitejs.dev" target="_blank">
    <img src={viteLogo} className="logo" alt="Vite logo" />
  </a>
</motion.div>
ドラッグ前の状態
ドラッグ前の状態

要素にマウスポインターを Hover すると拡大され、button 要素をドラッグすると画像と一緒にブラウザ上を自由に移動させることができます。

ドラッグ後の状態
ドラッグ後の状態

ドラッグの移動制限

ドラッグする範囲を X 軸、Y 軸というように制限することもできます。drag=“x”にすると X 軸のみのドラッグしか行うことができません。


<motion.div
  drag="x"
  whileHover={{
    scale: 1.5,
    transition: { duration: 1 },
  }}
>

dragConstrains propsにtop, right, left, bottomプロパティを設定することで移動できる範囲を制限することができます。


<motion.div
  drag
  dragConstraints={{ top: 30 }}
  whileHover={{
    scale: 1.5,
    transition: { duration: 1 },
  }}
>

デフォルトの位置から上に 30px 以上になると自動でその位置まで戻されます。left, right, bottom プロパティを設定していない場合は上(top)のみの制限となります。

要素を上に 30px 以上移動させてからデベロッパーツールで確認すると translateY の値が 30px になることが確認できます。

Dragの移動の制限
Dragの移動の制限

ロゴの中心から拡大を行いますが拡大を行う原点を設定したい場合には origin プロパティの値を分岐に利用することでエラーを設定することができます。

whileInView

ブラウザの画面のスクロールを行い、ViewPort に要素が入った時にアニメーションを実行したい場合に whileInView を利用することができます。


import viteLogo from '/vite.svg';
import './App.css';
import { motion } from 'framer-motion';

function App() {
  return (
    <>
      <div style={{ height: '100vh', marginButtom: '200px' }}>
        <motion.div
          whileHover={{
            scale: 1.2,
          }}
        >
          <a href="https://vitejs.dev" target="_blank">
            <img src={viteLogo} className="logo" alt="Vite logo" />
          </a>
        </motion.div>
      </div>
      <div>
        <motion.div
          initial={{ opacity: 0 }}
          transition={{ duration: 3 }}
          whileInView={{
            opacity: 1,
          }}
        >
          <a href="https://vitejs.dev" target="_blank">
            <img src={viteLogo} className="logo" alt="Vite logo" />
          </a>
        </motion.div>
      </div>
    </>
  );
}

export default App;

2 つ目のロゴ画像はページを開いた直後には initial props で opacity を 0 に設定しているので表示されていませんがスクロールを行い要素が ViewPort に入ると非表示から transition に設定した秒数でゆっくりと表示されます。再度上部にスクロールして 2 つ目のロゴが ViewPort から外し再度下にスクロールを行い ViewPort に入るとアニメーションによってゆっくりと表示されます。

Variantsの設定

Variants を利用してシンプルな例で動作確認を行います。variants では事前に状態の定義を行います。ここでは show と hidden で 2 つの状態を設定しています。show, hidden としていますが任意の名前をつけることができます。定義した variants を props としてアニメーションを行いたい要素に設定します。animate props の引数の中で useState による状態変化で定義した show と hidden の切り替えを行っています。


import './App.css';
import { motion } from 'framer-motion';
import { useState } from 'react';

const variants = {
  show: { opacity: 1, x: 0 },
  hidden: { opacity: 0, x: '-100%' },
};

function App() {
  const [isShow, setIsShow] = useState(true);

  return (
    <>
      <button onClick={() => setIsShow((isShow) => !isShow)}>Click</button>
      <motion.nav animate={isShow ? 'show' : 'hidden'} variants={variants}>
        <div>Hello</div>
      </motion.nav>
    </>
  );
}

export default App;

sShow の初期値は true なので animate には show の状態が指定されているため最初は Click ボタンと Hello が表示されています。Click ボタンをクリックすると Hello が左に移動しながら消えていきます。再度ボタンをクリックすると Hello が左側から表示されます。

variants の設定方法が理解できたので次は親子関係を持つ div 要素にアニメーションを設定しています。最初に variants を利用しない設定で動作確認を行います。

下記の設定では親要素に設定した Hello の文字列はゆっくりと非表示から表示になりますが 子要素に設定した World!は非表示から表示になると同時に 100px 右側にゆっくりと移動します。


import './App.css';
import { motion } from 'framer-motion';

function App() {
  return (
    <div className="App">
      <motion.div
        animate={{ opacity: 1 }}
        transition={{ duration: 3 }}
        initial={{ opacity: 0 }}
      >
        <h2>Hello</h2>
        <motion.div
          animate={{ opacity: 1, x: 50 }}
          transition={{ duration: 3 }}
          initial={{ opacity: 0 }}
        >
          <h2>World!</h2>
        </motion.div>
      </motion.div>
    </div>
  );
}

export default App;
アニメーション完了後の状態
アニメーション完了後の状態

varians を利用して書き換えます。helloVariants を定義します。プロパティには visible と hidden と名前をつけていますが任意の名前をつけることができます。visible と hidden プロパティで opacity の設定を行っています。アニメーションを行いたい要素に variants props を追加し作成した helloVariants を指定します。initial と animate props には helloVariants の中で設定した visible と hidden プロパティの名前を設定します。


import './App.css';
import { motion } from 'framer-motion';

const helloVariants = {
  visible: { opacity: 1 },
  hidden: { opacity: 0 },
};

function App() {
  return (
    <div className="App">
      <motion.div
        variants={helloVariants}
        animate="visible"
        transition={{ duration: 3 }}
        initial="hidden"
      >
        <h2>Hello</h2>
        <motion.div
          animate={{ opacity: 1, x: 50 }}
          transition={{ duration: 3 }}
          initial={{ opacity: 0 }}
        >
          <h2>World!</h2>
        </motion.div>
      </motion.div>
    </div>
  );
}

export default App;

varians設定前と同じアニメーションとなります。transitionについはvisibleプロパティに追加することができます。


const helloVariants = {
  visible: { opacity: 1, transition: { duration: 3 } },
  hidden: { opacity: 0 },
};

motion.divからtransitionを削除します。


<motion.div variants={helloVariants} animate="visible" initial="hidden">

variantsを利用していますがアニメーションの動作は同じままです。

子要素のmotion.divもvariantsを定義して設定します。worldVariansという名前をつけています。


import './App.css';
import { motion } from 'framer-motion';

const helloVariants = {
  visible: { opacity: 1, transition: { duration: 3 } },
  hidden: { opacity: 0 },
};

const worldVariants = {
  visible: { opacity: 1, x: 50, transition: { duration: 3 } },
  hidden: { opacity: 0 },
};

function App() {
  return (
    <div className="App">
      <motion.div variants={helloVariants} animate="visible" initial="hidden">
        <h2>Hello</h2>
        <motion.div variants={worldVariants} animate="visible" initial="hidden">
          <h2>World!</h2>
        </motion.div>
      </motion.div>
    </div>
  );
}

export default App;

最後に子要素である motion.div の animate, initial props を削除します。削除しても親要素で定義した visible と hidden が子要素に適用されるのではなく子要素で定義した worldVariants の hidden と visible が適用されます。


<motion.div variants={helloVariants} animate="visible" initial="hidden">
  <h2>Hello</h2>
  <motion.div variants={worldVariants}>
    <h2>World!</h2>
  </motion.div>
</motion.div>

子要素のvariantsの名前をvisible2とhidden2に変更します。


const worldVariants = {
  visible2: { opacity: 1, x: 50, transition: { duration: 3 } },
  hidden2: { opacity: 0 },
};

子要素には親要素のvariansで定義したvisibleとhiddenが適用されます。

beforeChildren

親要素のアニメーションが完了してから子要素のアニメーションを開始させたい場合に transition の when プロパティの beforeChildren を親要素の variants で設定することで実現できます。


const helloVariants = {
  visible: { opacity: 1, transition: { when: 'beforeChildren', duration: 3 } },
  hidden: { opacity: 0 },
};

const worldVariants = {
  visible: { opacity: 1, x: 50, transition: { duration: 3 } },
  hidden: { opacity: 0 },
};

動作確認を行うと Hello がアニメーションで表示されてから World!のアニメーションが開始されます。variants は親子関係を持つ要素で利用することができます。

staggerChildren

複数の子要素が存在する場合に一つの子要素のアニメーションが開始後しばらくしてから次の子要素のアニメーションを実施させたい場合に staggerChildren を利用することができます。Hello の文字列を含む子要素を 2 つ設定しています。


import './App.css';
import { motion } from 'framer-motion';

const helloVariants = {
  visible: {
    opacity: 1,
    transition: { when: 'beforeChildren', staggerChildren: 3, duration: 3 },
  },
  hidden: { opacity: 0 },
};

const worldVariants = {
  visible: { opacity: 1, x: 50, transition: { duration: 3 } },
  hidden: { opacity: 0 },
};

function App() {
  return (
    <div className="App">
      <motion.div variants={helloVariants} animate="visible" initial="hidden">
        <h2>Hello</h2>
        <motion.div variants={worldVariants}>
          <h2>World!</h2>
        </motion.div>
        <motion.div variants={worldVariants}>
          <h2>World!</h2>
        </motion.div>
      </motion.div>
    </div>
  );
}

export default App;

staggerChildrenで3(秒)を設定しているので1つ目の”World!”の文字列がアニメーションを開始してから3秒後に次の子要素のアニメーションが開始されます。

リスト表示

staggerChildren を利用して menu ボタンをクリックした後に menu 一覧を表示する設定します。


import { useState } from 'react';
import './App.css';
import { motion } from 'framer-motion';

const menuVariants = {
  show: {
    opacity: 1,
    transition: { staggerChildren: 0.1 },
  },
  hidden: { opacity: 0 },
};

const menus = ['Home', 'About', 'Profile', 'Product', 'Links'];

function App() {
  const [isShow, setIsShow] = useState(false);
  return (
    <div className="App">
      <button onClick={() => setIsShow((isShow) => !isShow)}>Menu</button>
      <motion.ul variants={menuVariants} animate={isShow ? 'show' : 'hidden'}>
        {menus.map((menu) => (
          <motion.li variants={menuVariants} key={menu}>
            {menu}
          </motion.li>
        ))}
      </motion.ul>
    </div>
  );
}

export default App;

リストを逆から表示したい場合には staggerDirection を利用することができます。


transition: { staggerChildren: 0.1, staggerDirection: -1 },

非表示にする際にリストの下から順番に非表示にしたい場合には以下のコードで実現することができます。


import { useState } from 'react';
import './App.css';
import { motion } from 'framer-motion';

const menuVariants = {
  show: {
    transition: { staggerChildren: 0.1 },
  },
  hidden: {
    transition: { staggerChildren: 0.1, staggerDirection: -1 },
  },
};

const listVariants = {
  show: {
    opacity: 1,
  },
  hidden: {
    opacity: 0,
  },
};

const menus = ['Home', 'About', 'Profile', 'Product', 'Links'];

function App() {
  const [isShow, setIsShow] = useState(false);
  return (
    <div className="App">
      <button onClick={() => setIsShow((isShow) => !isShow)}>Menu</button>
      <motion.ul variants={menuVariants} animate={isShow ? 'show' : 'hidden'}>
        {menus.map((menu) => (
          <motion.li variants={listVariants} key={menu}>
            {menu}
          </motion.li>
        ))}
      </motion.ul>
    </div>
  );
}

export default App;

SVG のアニメーション

サイドバーの表示・非表示などに頻繁に利用されるハンバーガーメニューを利用して SVG のアニメーションを行います。

3 本線のハンバガーメニューは svg タグを利用して下記のように作成します。


import './App.css';

function App() {
  return (
    <svg width="23" height="20" viewBox="0 0 23 20">
      <path
        fill="transparent"
        strokeWidth="3"
        stroke="hsl(0, 0%, 18%)"
        strokeLinecap="round"
        d="M 2 2.5 L 20 2.5"
      ></path>
      <path
        fill="transparent"
        strokeWidth="3"
        stroke="hsl(0, 0%, 18%)"
        strokeLinecap="round"
        d="M 2 9.423 L 20 9.423"
      ></path>
      <path
        fill="transparent"
        strokeWidth="3"
        stroke="hsl(0, 0%, 18%)"
        strokeLinecap="round"
        d="M 2 16.346 L 20 16.346"
      ></path>
    </svg>
  );
}

export default App;

ブラウザで確認すると下記のように 3 本線のハンバーガーメニューが表示されます。

ハンバーガーメニューの3本線
ハンバーガーメニューの3本線

通常は 3 本線ですが、クリックするとバッテンになるようにアニメーションを設定するのでバッテンの状態の svg も確認しておきます。上限の線は回転、真ん中の線は非表示にします。


import './App.css';

function App() {
  return (
    <svg width="23" height="20" viewBox="0 0 23 20">
      <path
        fill="transparent"
        strokeWidth="3"
        stroke="hsl(0, 0%, 18%)"
        strokeLinecap="round"
        d="M 3 16.5 L 17 2.5"
      ></path>
      <path
        fill="transparent"
        strokeWidth="3"
        stroke="hsl(0, 0%, 18%)"
        strokeLinecap="round"
        d="M 2 9.423 L 20 9.423"
        style={{ opacity: 0 }}
      ></path>
      <path
        fill="transparent"
        strokeWidth="3"
        stroke="hsl(0, 0%, 18%)"
        strokeLinecap="round"
        d="M 3 2.5 L 17 16.346"
      ></path>
    </svg>
  );
}

export default App;
3本線からバッテンへ
3本線からバッテンへ

3 本線からバッテンへの状態をアニメーションを利用して設定を行っていきます。

アニメーション設定後のコードは下記となります。


import { useState } from 'react';
import './App.css';
import { motion } from 'framer-motion';

function App() {
  const [open, setOpen] = useState(false);
  return (
    <motion.div animate={open ? 'open' : 'closed'}>
      <button onClick={() => setOpen((open) => !open)}>
        <svg width="23" height="20" viewBox="0 0 23 20">
          <motion.path
            fill="transparent"
            strokeWidth="3"
            stroke="hsl(0, 0%, 18%)"
            strokeLinecap="round"
            variants={{
              closed: { d: 'M 2 2.5 L 20 2.5' },
              open: { d: 'M 3 16.5 L 17 2.5' },
            }}
          ></motion.path>
          <motion.path
            fill="transparent"
            strokeWidth="3"
            stroke="hsl(0, 0%, 18%)"
            strokeLinecap="round"
            d="M 2 9.423 L 20 9.423"
            variants={{
              closed: { opacity: 1 },
              open: { opacity: 0 },
            }}
          ></motion.path>
          <motion.path
            fill="transparent"
            strokeWidth="3"
            stroke="hsl(0, 0%, 18%)"
            strokeLinecap="round"
            variants={{
              closed: { d: 'M 2 16.346 L 20 16.346' },
              open: { d: 'M 3 2.5 L 17 16.346' },
            }}
          ></motion.path>
        </svg>
      </button>
    </motion.div>
  );
}

export default App;

3 本線とバッテンの状態を管理するために useState Hook を利用しています。svg タグの 3 つの path タグは framer motion のアニメーションを設定するため motion.path に変更します。アニメーションの状態は variants props を利用して設定します。path それぞれアニメーションの設定は異なるので variants は異なるパスの値が設定されています。パスは先ほど確認した値です。真ん中の線は表示と非表示の切り替えなので opacity のみ設定を行っています。

クリック前は 3 本線の状態です。

クリック前の状態
クリック前の状態

ボタンをクリックするとアニメーションによって 3 本線からバッテンに変わります。再度ボタンをクリックするとアニメーションで 3 本線に戻ります。

クリック後の状態
クリック後の状態

ボタンのクリックによってサイドバーの表示・非表示を行いたい場合には framer motion を利用することで簡単に実現することができます。

デフォルトで適用されている CSS を解除するために main.jsx ファイルで import している index.css を削除しておきます。


import { useState } from 'react';
// import './App.css';
import { motion } from 'framer-motion';

function App() {
  const [open, setOpen] = useState(false);
  return (
    <>
      <motion.div animate={open ? 'open' : 'closed'}>
        <motion.div
          style={{
            backgroundColor: 'gray',
            position: 'absolute',
            top: 0,
            bottom: 0,
            left: 0,
            width: '200px',
          }}
          variants={{
            closed: { left: -200 },
            open: { left: 0 },
          }}
        >
          <div style={{ marginTop: '50px', color: 'white' }}>
            サイドメニュー
          </div>
        </motion.div>

        <button
          onClick={() => setOpen((open) => !open)}
          style={{ position: 'absolute', top: 10, left: 10 }}
        >
          <svg width="23" height="20" viewBox="0 0 23 20">
            <motion.path
              fill="transparent"
              strokeWidth="3"
              stroke="hsl(0, 0%, 18%)"
              strokeLinecap="round"
              variants={{
                closed: { d: 'M 2 2.5 L 20 2.5' },
                open: { d: 'M 3 16.5 L 17 2.5' },
              }}
            ></motion.path>
            <motion.path
              fill="transparent"
              strokeWidth="3"
              stroke="hsl(0, 0%, 18%)"
              strokeLinecap="round"
              d="M 2 9.423 L 20 9.423"
              variants={{
                closed: { opacity: 1 },
                open: { opacity: 0 },
              }}
            ></motion.path>
            <motion.path
              fill="transparent"
              strokeWidth="3"
              stroke="hsl(0, 0%, 18%)"
              strokeLinecap="round"
              variants={{
                closed: { d: 'M 2 16.346 L 20 16.346' },
                open: { d: 'M 3 2.5 L 17 16.346' },
              }}
            ></motion.path>
          </svg>
        </button>
      </motion.div>
      <main style={{ marginTop: '50px' }}>メイン</main>
    </>
  );
}

export default App;

ハンバーガーメニューが 3 本線の状態ではサイドメニューは非表示の状態です。

サイドメニューが非表示の状態
サイドメニューが非表示の状態

ボタンをクリックするとサイドメニューがアニメーションで左から表示されます。

サイドメニューが表示された状態
サイドメニューが表示された状態

ゆっくりとサイドバーを表示させたい場合には transition の duration を利用することができます。


variants={{
  closed: { left: -200, transition: { duration: 3 } },
  open: { left: 0, transition: { duration: 3 } },
}}

このように variants を利用することでいろいろな場所でアニメーションを利用することができます。

keyframsの設定

複数のアニメーションを実行したい場合に利用することができます。scaleプロパティの値に135を設定した場合は135度回転してアニメーションは終わりでしたが配列を設定することで複数の角度を設定することができます。


import viteLogo from '/vite.svg';
import './App.css';
import { motion } from 'framer-motion';

function App() {
  return (
    <div className="App">
      <motion.div
        animate={{ rotate: [0, 45, 135, 0] }}
        transition={{ duration: 2 }}
      >
        <a href="https://vitejs.dev" target="_blank">
          <img src={viteLogo} className="logo" alt="Vite logo" />
        </a>
      </motion.div>
    </div>
  );
}

export default App;

2秒間で45度回転してさらに2秒間で90度回転して再度に元の角度の0に戻ります。45度から135度に移動する場合に45+135で180度回転するわけではありません。

さらに回転と同時にサイズを変更したい場合にはscaleを利用してrotateと同じ要素の数を持つ配列でscaleの値を設定します。


function App() {
  return (
    <div className="App">
      <motion.div
        animate={{ scale: [1, 1.2, 1, 1.6], rotate: [0, 45, 135, 0] }}
        transition={{ duration: 2 }}
      >
        <a href="https://vitejs.dev" target="_blank">
          <img src={viteLogo} className="logo" alt="Vite logo" />
        </a>
      </motion.div>
    </div>
  );
}

useAnimate Hookの利用

framer motionではHookも提供されています。useAnimate Hookを利用して動作確認を行います。

useAnimate Hookを利用してロゴの回転を行う場合は下記のようにrefを使って要素そのものに直接アクセスして処理を行います。motion.divは必要ではありません。


import viteLogo from '/vite.svg';
import './App.css';
import { useAnimate } from 'framer-motion';
import { useEffect } from 'react';

function App() {
  const [scope, animate] = useAnimate();

  useEffect(() => {
    animate(scope.current, { rotate: 135 }, { duration: 3 });
  }, []);
  return (
    <div className="App">
      <div ref={scope}>
        <a href="https://vitejs.dev" target="_blank">
          <img src={viteLogo} className="logo" alt="Vite logo" />
        </a>
      </div>
    </div>
  );
}

export default App;

animate の第一引数は ref props を設定した要素そのものを回転させるため scope.current を利用しています。もし画像のみ回転させたい場合には scope.current 内に含まれている”img”を指定します。ref props で scope を設定した div 要素内に含まれる img 要素のみ回転します。


useEffect(() => {
  animate('img', { rotate: 135 }, { duration: 3 });
}, []);

img を指定している場合は div 要素の中に”Hello”の文字列を挿入しても画像のみ回転します。


<div ref={scope}>
  <a href="https://vitejs.dev" target="_blank">
    <img src={viteLogo} className="logo" alt="Vite logo" />
  </a>
</div>
画像のみ回転
画像のみ回転

animate の第一引数に scope.current を設定した場合にはアニメーションが div 要素全体に適用されるので画像と共に”Hello”の文字列も一緒に回転します。


useEffect(() => {
  animate(scope.current, { rotate: 135 }, { duration: 3 });
}, []);
文字列も一緒に回転
文字列も一緒に回転

useEffect を利用していましたが useEffect を利用せずボタンに click イベントを設定してボタンがクリックした時に回転のアニメーションを開始させるといったことも簡単に行えます。


import viteLogo from '/vite.svg';
import './App.css';
import { useAnimate } from 'framer-motion';
import { useEffect } from 'react';

function App() {
  const [scope, animate] = useAnimate();

  return (
    <div className="App">
      <button
        onClick={() => animate(scope.current, { rotate: 135 }, { duration: 3 })}
      >
        回転
      </button>
      <div ref={scope}>
        <h1>Hello</h1>
        <a href="https://vitejs.dev" target="_blank">
          <img src={viteLogo} className="logo" alt="Vite logo" />
        </a>
      </div>
    </div>
  );
}

export default App;

初めて Framer Motion にふれた人も Framer Motion を利用することで簡単にアニメーションが実装できるということが理解できたかと思います。