DEMO
CODE
<div class="demo-stage fpt-stage">
<div class="fpt-tabs">
<button class="fpt-tab fpt-tab-active" data-page="home" type="button">HOME</button>
<button class="fpt-tab" data-page="about" type="button">ABOUT</button>
</div>
<div class="fpt-view">
<div class="fpt-page fpt-in" data-page="home"><span>01</span>HOME PAGE</div>
<div class="fpt-page fpt-hidden" data-page="about"><span>02</span>ABOUT PAGE</div>
</div>
</div>
.fpt-stage { display:flex; flex-direction:column; align-items:center; justify-content:center; gap:1.1rem; padding:3rem 1.5rem; background:#0F0F12; min-height:300px; }
.fpt-tabs { display:flex; gap:.6rem; }
.fpt-tab {
padding:.5rem 1.4rem; border-radius:999px; cursor:pointer;
font-family:'Oswald',sans-serif; font-size:.72rem; letter-spacing:.2em;
color:#c9c9d1; background:transparent; border:1px solid rgba(255,255,255,.2);
transition: color .25s ease, border-color .25s ease;
}
.fpt-tab-active { color:#fff; border-color:#E91B89; background:rgba(233,27,137,.14); }
.fpt-view { position:relative; width:min(380px,100%); height:130px; }
.fpt-page {
position:absolute; inset:0; border-radius:14px;
display:flex; align-items:center; justify-content:center; gap:.8rem;
background:#1A1A1A; border:1px solid rgba(255,255,255,.08);
color:#fff; font-family:'Oswald',sans-serif; letter-spacing:.16em;
}
.fpt-page span { color:#E91B89; font-size:1.4rem; }
.fpt-hidden { display:none; }
.fpt-in { animation: fpt-enter .34s cubic-bezier(.16,1,.3,1) both; }
.fpt-out { animation: fpt-exit .22s ease both; }
@keyframes fpt-enter { from { opacity:0; transform:translateX(28px); } to { opacity:1; transform:translateX(0); } }
@keyframes fpt-exit { from { opacity:1; transform:translateX(0); } to { opacity:0; transform:translateX(-28px); } }
(function(){
var busy = false;
document.querySelectorAll('.fpt-tab').forEach(function(tab){
tab.addEventListener('click', function(){
if (busy || tab.classList.contains('fpt-tab-active')) return;
busy = true;
document.querySelectorAll('.fpt-tab').forEach(function(t){ t.classList.remove('fpt-tab-active'); });
tab.classList.add('fpt-tab-active');
var current = document.querySelector('.fpt-page:not(.fpt-hidden)');
var next = document.querySelector('.fpt-page[data-page="' + tab.dataset.page + '"]');
current.classList.remove('fpt-in');
current.classList.add('fpt-out');
current.addEventListener('animationend', function handler(){
current.removeEventListener('animationend', handler);
current.classList.remove('fpt-out');
current.classList.add('fpt-hidden');
next.classList.remove('fpt-hidden');
next.classList.add('fpt-in');
busy = false;
});
});
});
})();
ライブデモはFramer Motion不要のCSS/JS等価版(exit完了をanimationendで待ってから入場)。下がReact + Framer Motionの実装です。mode="wait" が「退場が終わってから入場」を自動化します。
import { AnimatePresence, motion } from "framer-motion"
import { usePathname } from "next/navigation"
export default function Template({ children }) {
const pathname = usePathname()
return (
<AnimatePresence mode="wait">
<motion.div
key={pathname}
initial={{ opacity: 0, x: 28 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -28 }}
transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1] }}
>
{children}
</motion.div>
</AnimatePresence>
)
}
AI PROMPT
ページ切り替え時に前のページが左へ退場してから次のページが右から入場するルート遷移をFramer Motionで作って。ページラッパーを <AnimatePresence mode="wait"> で包み、<motion.div key={pathname}> に initial={{ opacity: 0, x: 28 }} / animate={{ opacity: 1, x: 0 }} / exit={{ opacity: 0, x: -28 }} を指定。key がルートごとに変わることで退場と入場が発火し、mode="wait" が「退場完了→入場開始」の順序を保証する。Next.js App Router なら template.js に置く。CSSで近似するなら exit 用 keyframes の animationend を待ってから入場側を再生する。
Acceptance criteria: works with prefers-reduced-motion (animation disabled), zero console errors, zero layout shift
AIエージェントでの実装ガイド → Claude Code でWebアニメーションを実装する
BOOKS · PR · AMAZON
※当サイトはAmazonアソシエイトプログラムの参加者です。リンクから商品をご購入いただくと、当方に紹介料が発生します。
学びを止めない · AMAZONサブスク
※当サイトはAmazonアソシエイトプログラムの参加者です。リンクから商品をご購入いただくと、当方に紹介料が発生します。
SPAやNext.jsでルートを切り替えるとき、前のページが退場するアニメーションを見せてから次のページを入場させるページ遷移演出。AnimatePresence の mode="wait" と、ルートごとに変わる key の組み合わせで「退場完了→入場開始」の順序が保証されるのが核心です。
なお、CSSのみで実装する当サイトの Page Fade Transition はページ読み込み時の入場演出が対象で、本ページはSPAのルート切り替え(退場つき)が対象という違いがあります。MPAなら View Transition API という選択肢もあります。