Replacing Terminal Navigation with Page Transitions
A short fade on the committed page, with normal links, a stable header, and no animation for keyboard or history navigation.
In this article
Updated September 16, 2026
I originally built a terminal overlay for this portfolio because the command-line metaphor felt familiar. Clicking a link started an entrance animation, typed a command, and waited 450ms before calling the router. Even when the next page was ready, the visitor had to wait for the performance. The overlay also needed its own state, timers, and recovery logic.
I replaced it with a transition on the page content. Links start navigation normally; motion follows the route change.
Let the router navigate
Navigation uses ordinary Next.js Link components, including the header links. Their click handlers record analytics without intercepting navigation or scheduling a later router.push.
A stable PageTransition wrapper watches usePathname(). When a new pathname commits after pointer input, it uses the Web Animations API to fade the new page from 72% to full opacity in 180ms. Next.js continues to manage navigation, focus, and scroll.
This is the animation inside the layout effect, after the input and reduced-motion checks:
const animation = element.animate([{ opacity: 0.72 }, { opacity: 1 }], {
duration: 180,
easing: 'cubic-bezier(0.22, 1, 0.36, 1)',
})
const stop = () => animation.cancel()
preference.addEventListener('change', stop)
return () => {
stop()
preference.removeEventListener('change', stop)
}
The header sits outside the animated surface. Its logo and navigation stay in place while the content changes below them. The wrapper also provides the target for the “Skip to content” link.
Keep the new page in place
An earlier version captured the outgoing page and blurred and translated both pages during the change. It looked disjointed when the routes had different scroll positions. The current transition animates only the committed page. There is no outgoing snapshot, blur, or movement.
The first render stays still. So do section links, since changing a hash does not change the pathname. Keyboard navigation, Back and Forward, and reduced-motion preferences all skip the fade and show the committed content immediately.
Another route change or unmount cancels any running animation. Changing the reduced-motion preference cancels it too. The effect removes its listener during cleanup.
Preserve ordinary navigation
Normal links retain modified-click and new-tab behavior. The transition adds no delay before navigation, and the new page stays interactive throughout the fade.
The useful checks are practical: follow a case-study link, return with Back, jump to a section, navigate with the keyboard, and repeat with reduced motion enabled. Each should remain usable with the transition removed.