まえがき
React のアニメーションライブラリである Framer Motion を利用してページ遷移アニメーションを実装した際に、いくつか引っかかった点があったので、備忘録として残しておこうと思います。
バージョン
- Next.js v12.1.1
- Framer Motion v7.6.1
今回実装するもの
↑ こんな感じのふんわりしたフェードイン / フェードアウトを実装します。
手順
1. アンマウント時のアニメーションを有効にする
import { AnimatePresence } from 'framer-motion'
import type { AppProps } from 'next/app'
import 'styles/globals.css'
const MyApp = ({ Component, pageProps, router }: AppProps) => (
<AnimatePresence mode="wait">
<Component key={router.asPath} {...pageProps} />
</AnimatePresence>
)
export default MyApp
アンマウント時のアニメーションを有効にするため、framer-motion
から <AnimatePresence>
を import して、<Component>
を囲みます。
AnimatePresence
mode="wait"
とすることで、コンポーネントのアンマウントを待ち、遷移元のアンマウント時のアニメーションと遷移先のマウント時のアニメーションが同時に発生しないようにしています。
Component
key
の値として、初めは router.route
を利用していたのですが、この値は pages/
ディレクトリ以下のファイルパス そのもの であり、 Dynamic Routing を使用して生成しているページ([id].tsx
みたいなページ)では [id]
の部分が展開されず同じ値になってしまうので、「一部ページでアニメーションが再生されない」という問題が起きました。
ですので、ここではブラウザ上で表示されるパスである router.asPath
を利用しています。
// パス = pages/[id]
// id = hoge
// ...
console.log(router.route) // -> "pages/[id]"
console.log(router.asPath) // -> "pages/hoge"
2. 遷移アニメーションを設定
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. リンクを修正
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を修正
import { AnimatePresence } from 'framer-motion'
import type { AppProps } from 'next/app'
import 'styles/globals.css'
const MyApp = ({ Component, pageProps, router }: AppProps) => (
- <AnimatePresence mode="wait">
+ <AnimatePresence mode="wait" onExitComplete={() => window.scrollTo(0, 0)}>
<Component key={router.asPath} {...pageProps} />
</AnimatePresence>
)
export default MyApp
onExitComplete={() => window.scrollTo(0, 0)}
を追加して、アンマウント完了後にページを先頭まで戻すようにします。
これでOKです! 🎉
あとがき
Framer Motion について、かなりシンプルな記述でアニメーションを実装することができ、とても楽しいなという印象を持ちました。
2Dアニメーションだけでなく 3Dアニメーション も扱えたりするようなので、もう少し色々触ってみようと思います!