Back to work
tutorial//2 min read

Crafting Smooth Page Transitions

A deep dive into building performant, elegant page transitions using Framer Motion and Next.js App Router.


Page transitions are one of those details that separate a good interface from a great one. When done right, they're invisible — the user simply feels that the app is fluid. When done wrong, they create a jarring, disconnected experience.

The approach

The key insight is to animate layout, not just opacity. A simple fade feels flat. But when elements shift, scale, and settle into their new positions with spring physics, the interface feels alive.

<motion.div
  layoutId="work-card"
  transition={{ type: "spring", stiffness: 300, damping: 30 }}
>
  {children}
</motion.div>

layoutId tells Motion that two elements across renders are the same thing — so it morphs one into the other instead of crossfading.

Try it

Click a card. It expands into a detail panel with a shared-layout morph. Close it, and it settles back into the grid:

Click a card to open — watch the shared layout spring. Click outside or Close to morph back.

Why spring physics matter

Springs model real-world motion. Unlike cubic-bezier easing, springs don't have a fixed duration — they settle naturally based on stiffness and damping. This makes animations feel organic rather than mechanical.

Hover to lift. Same distance, three spring personalities.

Stiffness vs. Damping

  • High stiffness, low damping → bouncy, energetic
  • Low stiffness, high damping → slow, heavy
  • Medium stiffness, high damping → snappy, precise (usually what you want for UI)

Implementation tips

  1. Use layoutId for shared element transitions between routes
  2. Keep interactions under 300ms-feel; page morphs can breathe a bit longer
  3. Always respect prefers-reduced-motion
  4. Test on lower-end devices — 60fps is non-negotiable
  5. Wrap related morphs in <LayoutGroup> so they coordinate

The best transitions are the ones users don't notice. They just make everything feel right.