この記事の概要
モバイルアプリを触っているとビューの遷移にShared Element Transitionが使われていることが多いです。
Webだとあまりメジャーではない気がしますが、上手いことやってみたくてチャレンジしました。
ネイティブアプリほどのスムーズさは実現できませんでしたが、まあほどほど……くらいにはなったので1度記事にしてみます。
より上手くできた方がいたら是非教えてください。
執筆時の環境
パッケージ | バージョン |
---|---|
framer-motion | 6.3.6 |
next | 12.1.6 |
react | 18.1.0 |
react-dom | 18.1.0 |
完成形
最初に今回完成したものを載せておきます。
トップページに記事一覧がサムネイル表示されていて、クリックすると詳細ページへジャンプ。
共通の要素である画像とタイトルがページをまたいで変化する、というイメージです。
左下に映っている⌘[
は、ブラウザバックしたした瞬間を分かりやすくするために表示したショートカットです。
作り方
アニメーションなしのページを作る
まずはアニメーションのないページを作ります。
以下はトップページです。
スタイルは大したことを書いていないので省略します。
リンクのループを[...Array(12)].map((_, i)
で済ませたり、画像のファイル名を0.jpg
,1.jpg
と雑に命名したりしています。
あくまでアニメーションの検証用で書いたコードなのでここは適宜読み替えてください。
import Image from "next/image";
import Link from "next/link";
import styles from "../styles/Home.module.css";
export default function Home() {
return (
<div className={styles.container}>
<h1 className={styles.topTitle}>Home</h1>
{[...Array(12)].map((_, i) => (
<Link href={`/${i}`} key={i}>
<a className={styles.image}>
<div>
<Image src={`/${i}.jpg`} alt="" width={368} height={207} />
</div>
<h2 className={styles.imageName}>
Image {i}
</h2>
</a>
</Link>
))}
</div>
);
}
見た目はこんな感じです。
対して詳細のページは以下です。
こちらもファイル名から酷いものですが、目を瞑ってください。
現状はトランジションするものをイメージして<Image />
とその後の見出しを同じ見た目にしています。
見出しについては、トップページではh2
で詳細ページではh1
ですがここは問題ありません。
import Image from "next/image";
import { useRouter } from "next/router";
import styles from "../styles/Home.module.css";
export default function Id() {
const {
query: { id },
} = useRouter();
return (
<div className={styles.container}>
<div className={styles.centerImage}>
<Image src={`/${id}.jpg`} alt="" width={752} height={423} />
</div>
<h1 className={styles.pageTitle}>
Image {id}
</h1>
<div className={styles.paragraph}>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua.
</p>
<p>
minim veniam, quis nostrud exercitation ullamco laboris nisi ut
aliquip ex ea commodo consequat.
</p>
<p>
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum
dolore eu fugiat nulla pariatur.
</p>
<p>
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui
officia deserunt mollit anim id est laborum.
</p>
</div>
</div>
);
}
見た目はこんな感じです。
Framer Motionをインストールしてアニメーションをつける
React用アニメーションライブラリのFramer Motionをインストールします。
npm i framer-motion
まず、先ほどのコードのうち動かしたい要素をmotion.ElementName
の書式に変えます。
(対象がdiv
であればmotion.div
という具合です。)
<h1 className={styles.topTitle}>Home</h1>
{[...Array(12)].map((_, i) => (
<Link href={`/${i}`} key={i}>
<a className={styles.image}>
- <div>
+ <motion.div>
<Image src={`/${i}.jpg`} alt="" width={368} height={207} />
- </div>
+ </motion.div>
- <h2 className={styles.imageName}>
+ <motion.h2 className={styles.imageName}>
Image {i}
- </h2>
+ </motion.h2>
</a>
</Link>
))}
- <div className={styles.centerImage}>
+ <motion.div className={styles.centerImage}>
<Image src={`/${id}.jpg`} alt="" width={752} height={423} />
- </div>
+ </motion.div>
- <h1 className={styles.pageTitle}>
+ <motion.h1 className={styles.pageTitle}>
Image {id}
- </h1>
+ </motion.h1>
<div className={styles.paragraph}>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua.
</p>
<p>
minim veniam, quis nostrud exercitation ullamco laboris nisi ut
aliquip ex ea commodo consequat.
</p>
<p>
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum
dolore eu fugiat nulla pariatur.
</p>
<p>
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui
officia deserunt mollit anim id est laborum.
</p>
</div>
そうしたら、ページをまたいでも画像と見出しが紐付くようにlayoutId
を付与します。
これまた命名が雑ですが……雰囲気を感じてください。
どちらのページでも、画像はimage-0
image-1
image-2
となり、見出しも同様にtitle-0
title-1
title-2
となり、紐付きました。
<h1 className={styles.topTitle}>Home</h1>
{[...Array(12)].map((_, i) => (
<Link href={`/${i}`} key={i}>
<a className={styles.image}>
- <motion.div>
+ <motion.div layoutId={`image-${i}`}>
<Image src={`/${i}.jpg`} alt="" width={368} height={207} />
</motion.div>
- <motion.h2 className={styles.imageName}>
+ <motion.h2 layoutId={`title-${i}`} className={styles.imageName}>
Image {i}
</motion.h2>
</a>
</Link>
))}
- <motion.div className={styles.centerImage}>
+ <motion.div layoutId={`image-${id}`} className={styles.centerImage}>
<Image src={`/${id}.jpg`} alt="" width={752} height={423} />
</motion.div>
- <motion.h1 className={styles.pageTitle}>
+ <motion.h1 layoutId={`title-${id}`} className={styles.pageTitle}>
Image {id}
</motion.h1>
<div className={styles.paragraph}>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua.
</p>
<p>
minim veniam, quis nostrud exercitation ullamco laboris nisi ut
aliquip ex ea commodo consequat.
</p>
<p>
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum
dolore eu fugiat nulla pariatur.
</p>
<p>
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui
officia deserunt mollit anim id est laborum.
</p>
</div>
実はアニメーション自体は以上で完成です。
あとはFramer Motionが良い感じに遷移してくれます。
スクロール位置
ただ、このままだとスクロール位置がおかしなことになります。
下の方のリンクから詳細ページに遷移してブラウザバックすると、ページのトップに戻ってしまうのでやや違和感……。
というわけでnext.config.js
をいじります。
Create Next Appで生成された状態からの差分を以下に示します。
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
+ experimental: {
+ scrollRestoration: true
+ }
}
module.exports = nextConfig
experimental
ではありますが、scrollRestoration
をtrueにするとスクロールした位置を保持してくれます。
これで、冒頭の動画とほぼ同じ動きをするコードが完成しました。
無念な箇所
next/image
を使うとページが切り替わる際に一瞬ちらつきが発生してしまいました。
こちらのIssueを見るにかつては解決していたようなのですが、自分の手元では上手くいかず……。
画像のちらつきがなくなると更にスムーズに見えるとは思うのですが、現段階ではギブアップです。
最後まで読んでくださってありがとうございます!
Twitterでも情報を発信しているので、良かったらフォローお願いします!