Smooth Page Transitions in Next.js 15 – A Deep Dive Guide


Never miss an update
Subscribe to receive news and special offers.
By subscribing you agree to our Privacy Policy.
I've been building Next.js apps for years, and I've always struggled with page transitions. Framer Motion felt heavy. Custom solutions broke constantly. CSS animations never felt quite right.
Then Next.js 15 dropped the View Transitions API support, and everything changed.
The View Transitions API isn't another JavaScript animation library. It's a browser-native feature that handles transitions at the rendering level. Think hardware acceleration, minimal bundle size, and buttery smooth animations that don't block your UI.
The best part? Next.js made it ridiculously simple to implement.
# Choose your package manager
npm install next-view-transitions
yarn add next-view-transitions
pnpm add next-view-transitionsUpdate your next.config.js:
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
viewTransition: true,
},
};
module.exports = nextConfig;In your root layout, add the ViewTransitions provider:
import { ViewTransitions } from 'next-view-transitions'
export default function RootLayout({ children }) {
return (
<ViewTransitions>
<html lang="en">
<body className="font-sans">
{children}
</body>
</html>
</ViewTransitions>
)
}Now you need to use the special Link component and router hook for transitions to actually work. For links, replace Next.js's default Link with the transition-enabled version:
import { Link } from 'next-view-transitions'
export default function Navigation() {
return (
<nav className="flex space-x-4">
<Link href="/about" className="hover:text-blue-600">
About
</Link>
<Link href="/blog" className="hover:text-blue-600">
Blog
</Link>
<Link href="/contact" className="hover:text-blue-600">
Contact
</Link>
</nav>
)
}For programmatic navigation, use the transition router hook:
import { useTransitionRouter } from 'next-view-transitions'
export default function BlogPost() {
const router = useTransitionRouter()
const handleNavigation = () => {
// All Next.js router methods work with transitions
router.push('/blog/next-post')
// router.replace('/dashboard')
// router.back()
}
return (
<div className="prose max-w-none">
<h1>My Blog Post</h1>
<p>Content here...</p>
<button
onClick={handleNavigation}
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
>
Read Next Post
</button>
</div>
)
}That's it. You now have basic view transitions working.
The simplest transition is a fade. Add this CSS to your globals:
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 300ms;
animation-timing-function: ease-in-out;
}
::view-transition-old(root) {
animation-name: fade-out;
}
::view-transition-new(root) {
animation-name: fade-in;
}
@keyframes fade-out {
to { opacity: 0; }
}
@keyframes fade-in {
from { opacity: 0; }
}Want something more dynamic? Try slide transitions:
::view-transition-old(root) {
animation: slide-out-left 350ms ease-in-out;
}
::view-transition-new(root) {
animation: slide-in-right 350ms ease-in-out;
}
@keyframes slide-out-left {
to {
transform: translateX(-100%);
}
}
@keyframes slide-in-right {
from {
transform: translateX(100%);
}
}For a more modern feel, try scale transitions:
::view-transition-old(root) {
animation: scale-down 300ms ease-in-out;
}
::view-transition-new(root) {
animation: scale-up 300ms ease-in-out;
}
@keyframes scale-down {
to {
transform: scale(0.95);
opacity: 0;
}
}
@keyframes scale-up {
from {
transform: scale(1.05);
opacity: 0;
}
}You can create different transitions for different routes using CSS selectors:
/* Different transition for blog posts */
[data-route*="/blog"] ::view-transition-old(root) {
animation: slide-up 400ms ease-out;
}
[data-route*="/blog"] ::view-transition-new(root) {
animation: slide-down 400ms ease-out;
}Target specific elements for more granular control:
/* Transition specific components */
::view-transition-old(header),
::view-transition-new(header) {
animation-duration: 200ms;
}
::view-transition-old(main),
::view-transition-new(main) {
animation-duration: 400ms;
}Here's why this approach beats traditional solutions:
Bundle Size: Zero JavaScript for animations means smaller bundles Performance: Hardware-accelerated transitions that don't block rendering Compatibility: Graceful degradation for browsers without support Simplicity: No complex state management or animation libraries
The View Transitions API works in Chrome 111+, Edge 111+, and other Chromium browsers. For Firefox and Safari, the transitions simply won't occur - users get instant navigation instead.
This graceful degradation means you can implement this today without worrying about browser compatibility.
Here's a complete example of a blog with smooth transitions:
// app/layout.js
import { ViewTransitions } from 'next-view-transitions'
import './globals.css'
export default function RootLayout({ children }) {
return (
<ViewTransitions>
<html lang="en">
<body className="bg-white text-gray-900">
<nav className="border-b border-gray-200 p-4">
<div className="max-w-4xl mx-auto flex justify-between items-center">
<h1 className="text-xl font-bold">My Blog</h1>
<div className="space-x-4">
<a href="/" className="hover:text-blue-600">Home</a>
<a href="/blog" className="hover:text-blue-600">Blog</a>
<a href="/about" className="hover:text-blue-600">About</a>
</div>
</div>
</nav>
<main className="max-w-4xl mx-auto p-4">
{children}
</main>
</body>
</html>
</ViewTransitions>
)
}With the CSS transitions from earlier, every navigation feels smooth and intentional.
View Transitions are still experimental in Next.js, but they're stable enough for production use. As browser support improves, we'll see more advanced features like:
Good animations aren't just eye candy. They provide visual continuity that helps users understand your app's navigation flow. When done right, they make your app feel faster and more responsive.
The View Transitions API gives us that polish without the complexity or performance cost of traditional animation libraries.
Want more Next.js tips and tutorials? Subscribe to my newsletter for weekly deep dives into modern web development.