mcpField
P
page-transition-animation

page-transition-animation

github.com/iart-aiデザイン・UI632.5k 回インストール18 週間前に更新
リポジトリ

page-transition-animation について

page-transition-animationは、Next.js App Routerのページ遷移を実装・デバッグするスキルです。ナビゲーション時にAnimatePresenceの退出演出が動かない原因を解説し、FrozenRouterやView Transitions APIの方法を示します。

主な機能

  1. 01Next.js App Routerの遷移ガイド
  2. 02AnimatePresence退出演出のデバッグ
  3. 03FrozenRouterによるルート内容の保持
  4. 04View Transitions APIの方法
  5. 05遷移検証とアクセシビリティの指針

活用シーン

  1. 01App Routerのページに入場・退出演出を加える
  2. 02遷移時にページが即座に消える問題を調べる
  3. 03Framer MotionとView Transitionsを選ぶ
  4. 04遷移方向やクロスフェード、表示内容を確認する

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/iart-ai/web-animation-skills --skill page-transition-animation

インストール後、Claude Code で/page-transition-animationを入力して読み込まれたか確認します。

同梱ファイルREADME.mdreferences/full-examples.md

詳細

ページ遷移アニメーション(Next.js App Router)

Next.js App Router でページの入場・退出時の遷移を実装し、最もよくある問題、つまりナビゲーション時に Framer Motion の AnimatePresence の退出アニメーションがまったく実行されない問題を解決します。原因は構造にあります。App Router では、ナビゲーション時に Next.js が古いルートのコンテンツをアンマウントし、新しいルートをほぼ即座にマウントします。AnimatePresence が退出をアニメーションできるのは、永続する AnimatePresence ラッパーの下で、退出する要素がアニメーションの実行に十分な時間マウントされたままの場合だけです。ルーターがそのラッパーの下で子要素を入れ替えるため、退出アニメーションはスキップされます。この問題は、pathname をキーにした AnimatePresence と FrozenRouter パターン、または View Transitions API(ネイティブ/next-view-transitions)で解決できます。

使用する場面

Next.js App Router アプリにページ遷移アニメーションを追加するとき、ルート変更時に Framer Motion の exit prop が機能しないとき、古いページが退出アニメーションなしで瞬時に消えるとき、Next.js で Framer Motion と View Transitions API のどちらを使うか選ぶとき、またはルート間で AnimatePresence がアニメーションしない理由をデバッグするときに使用します。

退出アニメーションが実行されない理由(根本的な問題)

AnimatePresence は、直接のキー付き子要素が削除されたことを検出します。App Router のナビゲーションでは、次の点が重要です。

  1. ラッパーはナビゲーションをまたいで維持される必要があります。AnimatePresence を、それ自体がアンマウントされるコンポーネントに置くと、退出を実行するものが残りません。
  2. AnimatePresence が「古いものが削除され、新しいものが追加された」と認識できるように、子要素の key はルートごとに変わる必要があります。
  3. 短い重なりの間、退出するサブツリーは古いコンテンツを引き続きレンダリングする必要があります。しかし、App Router はすでにルートのコンテキストを切り替えているため、そのままでは退出するツリーが新しいページのデータをレンダリングしてしまいます。これを解決するのが FrozenRouter です。

解決策 A:template.tsx + キー付き AnimatePresence + FrozenRouter

template.tsx は App Router がこの用途のために用意した仕組みです。維持される layout.tsx とは異なり、テンプレートはナビゲーションのたびに再マウントされるため、ルートごとに新しいインスタンスが作られます。ルートごとの入場アニメーションに最適です。pathname をキーにした AnimatePresence と FrozenRouter を組み合わせることで、スムーズな退出と入場を実現できます。

// app/template.tsx
'use client';
import { AnimatePresence } from 'framer-motion';
import { usePathname } from 'next/navigation';
import { FrozenRouter } from './frozen-router';
import { PageTransition } from './page-transition';

export default function Template({ children }: { children: React.ReactNode }) {
  const pathname = usePathname();
  return (
    <AnimatePresence mode="wait" initial={false}>
      {/* key on pathname so AnimatePresence sees old removed / new added */}
      <PageTransition key={pathname}>
        {/* FrozenRouter keeps the OUTGOING tree rendering its old content
            while it animates out */}
        <FrozenRouter>{children}</FrozenRouter>
      </PageTransition>
    </AnimatePresence>
  );
}
// app/page-transition.tsx
'use client';
import { motion } from 'framer-motion';

export function PageTransition({ children }: { children: React.ReactNode }) {
  return (
    <motion.div
      initial={{ opacity: 0, y: 12 }}
      animate={{ opacity: 1, y: 0 }}
      exit={{ opacity: 0, y: -12 }}
      transition={{ duration: 0.3, ease: [0.22, 1, 0.36, 1] }}
    >
      {children}
    </motion.div>
  );
}
// app/frozen-router.tsx
'use client';
import { useContext, useRef } from 'react';
import {
  LayoutRouterContext,
} from 'next/dist/shared/lib/app-router-context.shared-runtime';

// Freezes the router context so the exiting page keeps rendering its
// OWN content during the exit animation instead of the next route's.
export function FrozenRouter({ children }: { children: React.ReactNode }) {
  const context = useContext(LayoutRouterContext ?? {});
  const frozen = useRef(context).current;

  return (
    <LayoutRouterContext.Provider value={frozen}>
      {children}
    </LayoutRouterContext.Provider>
  );
}

仕組みは次のとおりです。

  • mode="wait" を指定すると、新しいページをマウントする前に AnimatePresence が退出を完全に終了します。(入場と退出を重ねる/クロスフェードさせる場合は、デフォルトのモードを使用してください。)
  • initial={false} を指定すると、初回読み込み時の入場アニメーションをスキップします(任意)。
  • key={pathname} によって、AnimatePresence は各ルートを個別の presence として扱います。
  • FrozenRouter は LayoutRouterContext のスナップショットを保持するため、退出中のサブツリーは新しいルートのコンテンツを表示するのではなく、退出が終わるまで以前のルートのコンテンツをレンダリングします。これがないと、退出がスキップされるか、誤ったコンテンツが表示されます。

注:LayoutRouterContext は Next.js の内部機能です。インポートパスは Next のバージョンによって変わる場合があります。アップグレード後にインポートでエラーが起きた場合は、このパスを更新してください。

解決策 B:ネイティブの View Transitions API

View Transitions API は、ブラウザーが遷移前後のスナップショットをキャプチャし、2 つの DOM 状態間をアニメーションします。要素ごとの退出コンポーネントは必要ありません。

/* globals.css */
@view-transition { navigation: auto; } /* opt MPA-style in (where supported) */

::view-transition-old(root) { animation: fade 0.25s both reverse; }
::view-transition-new(root) { animation: fade 0.25s both; }
@keyframes fade { from { opacity: 0; } to { opacity: 1; } }

SPA スタイルの App Router ナビゲーションでは、ルーターの更新をラップします。

if (document.startViewTransition) {
  document.startViewTransition(() => router.push(href));
} else {
  router.push(href);
}
next-view-transitions ライブラリ

next-view-transitions は View Transitions API を App Router のクライアントナビゲーションと統合するため、すぐに使えます。

// app/layout.tsx
import { ViewTransitions } from 'next-view-transitions';
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <ViewTransitions>
      <html lang="en"><body>{children}</body></html>
    </ViewTransitions>
  );
}
// use the library's Link instead of next/link
import { Link } from 'next-view-transitions';
<Link href="/about">About</Link>;

シンプルなページ間のフェード/モーフや共有要素の遷移を最小限の JavaScript で実現する場合は View Transitions を選びます。細かな制御や、オーケストレーション、割り込み可能なアニメーション、または段階的な退出アニメーションが必要な場合は Framer Motion を選びます。View Transitions API のブラウザー対応状況にはまだばらつきがあるため、適切なフォールバック(if (document.startViewTransition) ガード)を用意してください。

AnimatePresence のデバッグチェックリスト

exit が発火しない場合は、次の順に確認します。

  1. ラッパーがマウントされたままであること。 AnimatePresence は、永続的な要素内に配置する必要があります。理想的には template.tsx、またはレイアウトレベルのクライアントコンポーネントに配置し、アンマウントされるページ内には配置しません。
  2. 安定していて、変更されるキーがあること。 アニメーション対象の子には、削除を検出できるように key={pathname}(または同様のもの)が必要です。キーがない、またはキーが変化しない場合、exit は実行されません。
  3. motion の子孫が直接配置されていること。 exit プロパティを持つ要素は motion.* コンポーネントであり、AnimatePresence の子である必要があります(間に motion 以外のラッパーがあると、検出が妨げられることがあります)。
  4. 古いビューと新しいビューを重ねたくない場合は mode="wait" を指定します。指定しない場合は両方が同時に描画され、切り替わることがあります。
  5. exit 中に遷移元のページに新しいルートのコンテンツが一瞬表示される場合は、FrozenRouter があることを確認します。
  6. AnimatePresence/usePathname/motion を使うすべてのファイルに 'use client' があることを確認します。
  7. mode="wait" の下に子が一度に 1 つだけあること。 AnimatePresence は、キー付きの presence を 1 つずつ切り替えることを想定しています。

配信と検証(スタンドアロン HTML)

パッケージ化されたヘルパー(scripts/):scripts/seek-shot.sh anim.html 0 1.5 3 は ?t=N のハーネスを固定し、各時点のスクリーンショットを撮影します。scripts/contact-sheet.sh sheet.png frame-*.png はそれらを並べ、ひと目で確認できる一覧を作成します。scripts/README.md を参照してください。

実際の対象は Next.js アプリですが、遷移の形状(入場/退場のカーブ、方向、クロスフェード)を実証するには、ブラウザーで直接開ける HTML ファイル 1 つを用意できます。2 つのスタブ「ページ」を切り替え、CDN の motion またはネイティブの View Transitions API でアニメーションさせます。ルーターを組み込む前にモーションを検証するには、ファイル 1 つが最適な段階です。フェードを目で確かめるだけのために Next を起動する必要はありません。

出力要件:

  • .html ファイル 1 つ。2 つのビューのスタブと切り替え機能を含めます。アニメーションには、CDN から motion/React をインポートするインラインの <script type="module">、または document.startViewTransition と ::view-transition-* CSS のいずれかを使います。
  • 解決済みの最終状態を固定する方法を用意します。ルート遷移は状態によって決まるため(どのビューか、切り替え途中か)、時計ではなく状態をスクリーンショットに撮ります。

シークハーネス — 遷移フェーズを固定する。 ?view=b で遷移先のビューをマウントします(または ?phase=exit で遷移元のツリーをアニメーション途中で保持します)。これにより、スクリーンショットを安定したフェーズで撮影できます。

<script>
  const p = new URLSearchParams(location.search);
  document.documentElement.dataset.view = p.get("view") || "a";   // CSS/React keys off this
  // Framer Motion: set the controlled key/route from ?view; for mode="wait" the resolved end is the new page
  // View Transitions: screenshot the END state (snapshots are transient); or pause via emulate slow animations
  window.__ready = true;
</script>

検証ループ — 描画 → フェーズ設定 → スクリーンショット → 確認: ソース、途中、遷移先(?view=a、?phase=exit、?view=b)を開き、それぞれスクリーンショットを撮影して、忠実度(exit が実際に実行されること、方向/クロスフェードが要件に一致すること、mode="wait" による遅延がないこと)と不具合(遷移元のツリーに新しいページのコンテンツが一瞬表示される、二重マウント、FOUC、ジャンク)を確認します。ヘッドレスツールなら何でも使えます。

npx playwright screenshot --wait-for-timeout=500 "file://$PWD/transition.html?view=b" dest.png

完了前に確認すること:

  1. スタンドアロンで開けること — コンソールエラーがなく、CDN の motion/React(使用する場合)が解決され、startViewTransition がガードされていること。
  2. ?view=/?phase= による固定で、遷移フェーズが決定論的になること。
  3. ソース/途中/遷移先でスクリーンショットを撮影し、要件に一致すること。コンテンツのフラッシュや exit のスキップがないこと。
  4. prefers-reduced-motion が尊重されること — スライド/移動を取り除き、短いクロスフェードにすること。
  5. イージングが意図的に設定されていること — 入場は ease-out、退場は ease-in、duration は約 0.2~0.4s(mode="wait" でも遅延を感じさせないこと)。

クイックリファレンス

必要なもの方法
ルートごとの入場アニメーションapp/template.tsx(ナビゲーションごとに再マウント)
ナビゲーション時の退場アニメーションキー付きの AnimatePresence mode="wait" + FrozenRouter
遷移元のページに古いコンテンツを表示するFrozenRouter(LayoutRouterContext のスナップショット)
ルート変更を検出するusePathname() を key として使う
ページ間のシンプルなフェード/モーフView Transitions API / next-view-transitions
共有要素の遷移view-transition-name CSS
初回読み込み時のアニメーションをスキップするinitial={false}

注意点

  • layout.tsx は保持され、ルートごとに再マウントされることはありません。ルートごとの入場アニメーションには template.tsx を使用してください。
  • FrozenRouter を省略すると、アニメーションの途中で退出中のページに新しいルートのコンテンツが表示される(ちらつきが発生する)か、退出アニメーションが完全にスキップされます。
  • LayoutRouterContext のインポートパスは Next 内部のものです。バージョン間で変更される可能性があるため、アップグレード後にまず修正が必要になる箇所です。
  • key={pathname} がないと、AnimatePresence は同じ子要素だと認識し、退出アニメーションが一度も実行されません。
  • mode="wait" は退出が終わるまで入場を待機します。退出に時間がかかる場合に多用すると、ナビゲーションが遅く感じられるため、再生時間を調整してください(約0.2~0.4秒)。
  • View Transitions API はブラウザーのサポートが十分ではありません。必ず document.startViewTransition をチェックしてから使用してください。
  • すべてのトランジションファイルで 'use client' が必要です。サーバーコンポーネントでは AnimatePresence/usePathname を使用できません。

参考ファイル

  • references/full-examples.md — 方向指定/スライドトランジション、view-transition-name を使った共有要素の View Transitions、mode="wait" を使わないクロスフェードのバリエーション、loading.tsx を使った読み込み状態のトランジション、および App Router の完全な動作例を含むフォルダー構成です。

コメント