fade / slide / zoom / push · 300–350ms · mode wait
Page Transitions
fade・slide・zoom・push の 4 パターンの画面遷移を切り替えて比較する。AnimatePresence mode="wait" で退場完了後に入場する。
opacity only · 安全
Page A
Dashboard summary view
/**
* Page Transitions — fade / slide / zoom / push の4遷移パターンを切り替えて比較。
* 移植元: motion-playground app/_sections/PageTransitions.tsx(dark 変換 M-B-2)
* tab + 次のページへボタンのユーザー操作駆動のため useLabReplay は不要。
*/
import { AnimatePresence, motion } from 'motion/react';
import { useState } from 'react';
import { easings } from '../../../lib/motion-tokens/playground';
type Variant = 'fade' | 'slide' | 'zoom' | 'push';
const variants: { key: Variant; label: string; note: string }[] = [
{ key: 'fade', label: 'Fade', note: 'opacity only · 安全' },
{ key: 'slide', label: 'Slide', note: 'horizontal push · context shift' },
{ key: 'zoom', label: 'Zoom', note: 'scale focus · drill-down' },
{ key: 'push', label: 'Push', note: 'stack push · iOS-like' },
];
function getVariants(variant: Variant) {
switch (variant) {
case 'fade':
return {
initial: { opacity: 0 },
animate: { opacity: 1 },
exit: { opacity: 0 },
transition: { duration: 0.3, ease: easings.gentle },
};
case 'slide':
return {
initial: { x: 60, opacity: 0 },
animate: { x: 0, opacity: 1 },
exit: { x: -60, opacity: 0 },
transition: { duration: 0.35, ease: easings.md3Emphasized },
};
case 'zoom':
return {
initial: { scale: 0.92, opacity: 0 },
animate: { scale: 1, opacity: 1 },
exit: { scale: 1.08, opacity: 0 },
transition: { duration: 0.3, ease: easings.md3Standard },
};
case 'push':
return {
initial: { y: 24, opacity: 0 },
animate: { y: 0, opacity: 1 },
exit: { y: -24, opacity: 0 },
transition: { duration: 0.35, ease: easings.md3Emphasized },
};
}
}
export default function PageTransitions() {
const [variant, setVariant] = useState<Variant>('fade');
const [page, setPage] = useState(0);
const v = getVariants(variant);
const pages = [
{
title: 'Page A',
body: 'Dashboard summary view',
bg: 'from-sky-500/10 to-indigo-500/10',
},
{
title: 'Page B',
body: 'Detail view of selected item',
bg: 'from-amber-500/10 to-rose-500/10',
},
{
title: 'Page C',
body: 'Settings & preferences',
bg: 'from-emerald-500/10 to-teal-500/10',
},
];
return (
<div data-lab-island className="w-full p-4 lg:p-6">
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
<div role="tablist" className="flex flex-wrap gap-1.5">
{variants.map((item) => (
<button
key={item.key}
type="button"
role="tab"
aria-selected={variant === item.key}
onClick={() => setVariant(item.key)}
className={`rounded-md px-3 py-1.5 text-xs font-medium transition-colors ${
variant === item.key
? 'bg-[var(--text-primary)] text-[var(--text-inverted)]'
: 'bg-[var(--bg-surface)] text-[var(--text-secondary)] ring-1 ring-[var(--border-subtle)] hover:bg-[var(--bg-elevated)]'
}`}
>
{item.label}
</button>
))}
</div>
<button
type="button"
onClick={() => setPage((p) => (p + 1) % pages.length)}
className="rounded-md border border-[var(--border-subtle)] bg-[var(--bg-surface)] px-3 py-1.5 text-sm text-[var(--text-secondary)] transition-colors hover:bg-[var(--bg-elevated)]"
>
次のページへ →
</button>
</div>
<p className="mb-3 text-xs text-[var(--text-muted)]">
{variants.find((x) => x.key === variant)?.note}
</p>
<div className="relative h-56 overflow-hidden rounded-xl border border-[var(--border-subtle)] bg-[var(--bg-surface)]">
<AnimatePresence mode="wait">
<motion.div
key={`${variant}-${page}`}
{...v}
className={`absolute inset-0 flex flex-col items-center justify-center bg-gradient-to-br ${pages[page].bg}`}
>
<p className="text-2xl font-semibold text-[var(--text-primary)]">
{pages[page].title}
</p>
<p className="mt-1 text-sm text-[var(--text-secondary)]">{pages[page].body}</p>
</motion.div>
</AnimatePresence>
</div>
</div>
);
} Implement page transitions with motion/react:
- fade: opacity 0 -> 1 (exit 1 -> 0), 300ms, cubic-bezier(0.4, 0, 0.2, 1)
- slide: translateX 60px -> 0 (exit -60px) + fade, 350ms, cubic-bezier(0.05, 0.7, 0.1, 1)
- zoom: scale 0.92 -> 1 (exit 1.08) + fade, 300ms, cubic-bezier(0.2, 0, 0, 1)
- push: translateY 24px -> 0 (exit -24px) + fade, 350ms, cubic-bezier(0.05, 0.7, 0.1, 1)
- AnimatePresence mode="wait" so the exiting page fully leaves before the next mounts
- key the page element by `${variant}-${page}` to re-trigger on either change