Next.js + Framer Motion でページ遷移アニメーションを実装する


まえがき

React のアニメーションライブラリである Framer Motion を利用してページ遷移アニメーションを実装した際に、いくつか引っかかった点があったので、備忘録として残しておこうと思います。

バージョン

  • Next.js v12.1.1
  • Framer Motion v6.2.8

今回実装するもの

↑ こんな感じのふんわりしたフェードイン / フェードアウトを実装します。

実際に動いているところは以下のサイトでご覧いただけます…!

手順

1. アンマウント時のアニメーションを有効にする

_app.tsx
import { AnimatePresence } from 'framer-motion'
import type { AppProps } from 'next/app'

import 'styles/globals.css'

const MyApp = ({ Component, pageProps, router }: AppProps) => (
  <AnimatePresence exitBeforeEnter>
    <Component key={router.asPath} {...pageProps} />
  </AnimatePresence>
)

export default MyApp

アンマウント時のアニメーションを有効にするため、framer-motion から <AnimatePresence> を import して、<Component> を囲みます。

AnimatePresence

exitBeforeEnter を設定することで、コンポーネントのアンマウントを待ち、遷移元のアンマウント時のアニメーションと遷移先のマウント時のアニメーションが同時に発生しないようにしています。

Component

key の値として、初めは router.route を利用していたのですが、この値は pages/ 内でのパスであり Dynamic Routing を使用して生成しているページでは同じ値 (例: works/[id]) になってしまうため、「該当のページでアニメーションが再生されない」という問題が起きました。

ですので、ここではブラウザ上で表示されるパスである router.asPathを利用しています。

2. 遷移アニメーションを設定

hoge.tsx
import { motion } from 'framer-motion'
import type { NextPage } from 'next'

const Hoge = (): NextPage => (
  <motion.div
    initial={{ opacity: 0 }} // 初期状態
    animate={{ opacity: 1 }} // マウント時
    exit={{ opacity: 0 }}    // アンマウント時
  >
   // ...
  </motion.div>
)

export default Hoge

全体を <motion> コンポーネントで囲み、任意の遷移アニメーションを設定します。

詳しい Props については、Motion components をご覧ください...。

3. ページ遷移時の挙動を修正

ここまででアニメーション自体は実装できているのですが、スクロールしてからページ遷移を行うと、「アニメーション再生前にページが先頭に戻る」という挙動になってしまいます。

これでは少し不恰好なので、修正していきます。

1. リンクを修正

fuga.tsx
import Link from 'next/link'

const Fuga = (): JSX.Element => (
  <div>
-   <Link href="https://example.com/">
+   <Link href="https://example.com/" scroll={false}>
      リンク!
    </Link>
  </div>
)

export default Fuga

ページの遷移が発生する箇所の <Link> コンポーネントに scroll={false} を追加して、遷移後の自動スクロールを無効化します。

2. _app.tsxを修正

_app.tsx
import { AnimatePresence } from 'framer-motion'
import type { AppProps } from 'next/app'

import 'styles/globals.css'

const MyApp = ({ Component, pageProps, router }: AppProps) => (
- <AnimatePresence exitBeforeEnter>
+ <AnimatePresence exitBeforeEnter onExitComplete={() => window.scrollTo(0, 0)}>
    <Component key={router.asPath} {...pageProps} />
  </AnimatePresence>
)

export default MyApp

onExitComplete={() => window.scrollTo(0, 0)} を追加して、アンマウント完了後にページを先頭まで戻すようにします。

これでOKです! 🎉

あとがき

Framer Motion について、かなりシンプルな記述でアニメーションを実装することができ、とても楽しいなという印象を持ちました。

2Dアニメーションだけでなく 3Dアニメーション も扱えたりするようなので、もう少し色々触ってみようと思います!

参考