Parallel & Intercepting Routes
Introduction to Parallel & Intercepting Routes
Next.js 13+ introduces powerful routing patterns that allow you to display multiple pages simultaneously and intercept navigation between routes. These advanced patterns enable sophisticated UI behaviors like modals, split views, and complex dashboards.
Parallel Routes
Parallel routes allow you to render multiple pages in the same layout simultaneously. This is particularly useful for dashboards, split views, and complex UI patterns where different sections need independent loading states and error handling.
Slots: Parallel routes are defined using named slots, which are convention-based folders starting with the @ symbol (e.g., @team, @analytics).
Basic Parallel Routes Structure
app/
dashboard/
@team/ # Named slot for team view
page.tsx
@analytics/ # Named slot for analytics view
page.tsx
layout.tsx # Parent layout receiving both slots
page.tsx # Default page
Implementing Parallel Routes
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
team, // @team slot
analytics // @analytics slot
}: {
children: React.ReactNode;
team: React.ReactNode;
analytics: React.ReactNode;
}) {
return (
<div className="dashboard-layout">
<div className="sidebar">{children}</div>
<div className="main-content">
<div className="team-section">{team}</div>
<div className="analytics-section">{analytics}</div>
</div>
</div>
);
}
// app/dashboard/@team/page.tsx
export default async function TeamSlot() {
const teamData = await fetchTeamData();
return (
<div>
<h2>Team Overview</h2>
<ul>
{teamData.map(member => (
<li key={member.id}>{member.name}</li>
))}
</ul>
</div>
);
}
// Independent loading state
export default function Loading() {
return <div>Loading team data...</div>;
}
// app/dashboard/@analytics/page.tsx
export default async function AnalyticsSlot() {
const analytics = await fetchAnalytics();
return (
<div>
<h2>Analytics Dashboard</h2>
<div className="stats">
<div>Views: {analytics.views}</div>
<div>Users: {analytics.users}</div>
</div>
</div>
);
}
Tip: Each parallel route can have its own loading.tsx, error.tsx, and not-found.tsx files, providing granular control over UI states.
Default Files for Parallel Routes
When navigating to a route that doesn't match all slots, Next.js needs to know what to render. Use default.tsx files to handle this:
app/
dashboard/
@team/
page.tsx
default.tsx # Fallback for @team slot
@analytics/
page.tsx
default.tsx # Fallback for @analytics slot
layout.tsx
// app/dashboard/@team/default.tsx
export default function DefaultTeam() {
return <div>Select a team to view details</div>;
}
Intercepting Routes
Intercepting routes allow you to load a route from another part of your application within the current layout. This is perfect for modals, previews, and maintaining context while navigating.
Convention: Intercepting routes use special folder naming conventions:
(.)- Same level(..)- One level up(..)(..)- Two levels up(...)- From root
Modal Pattern with Intercepting Routes
app/
photos/
page.tsx # Photo gallery
[id]/
page.tsx # Full photo page
@modal/
(.)photos/
[id]/
page.tsx # Intercepted modal view
default.tsx
layout.tsx
// app/layout.tsx
export default function RootLayout({
children,
modal
}: {
children: React.ReactNode;
modal: React.ReactNode;
}) {
return (
<html>
<body>
{children}
{modal}
</body>
</html>
);
}
// app/photos/page.tsx
import Link from 'next/link';
export default function PhotoGallery() {
const photos = [1, 2, 3, 4, 5];
return (
<div className="gallery">
<h1>Photo Gallery</h1>
<div className="grid">
{photos.map(id => (
<Link key={id} href={`/photos/${id}`}>
<img src={`/photo-${id}.jpg`} alt={`Photo ${id}`} />
</Link>
))}
</div>
</div>
);
}
// app/@modal/(.)photos/[id]/page.tsx
import { Modal } from '@/components/Modal';
export default function PhotoModal({ params }: { params: { id: string } }) {
return (
<Modal>
<img
src={`/photo-${params.id}.jpg`}
alt={`Photo ${params.id}`}
className="modal-image"
/>
</Modal>
);
}
// components/Modal.tsx
'use client';
import { useRouter } from 'next/navigation';
import { useEffect, useRef } from 'react';
export function Modal({ children }: { children: React.ReactNode }) {
const router = useRouter();
const dialogRef = useRef<HTMLDialogElement>(null);
useEffect(() => {
dialogRef.current?.showModal();
}, []);
const closeModal = () => {
router.back();
};
return (
<dialog
ref={dialogRef}
className="modal"
onClose={closeModal}
onClick={(e) => {
if (e.target === dialogRef.current) {
closeModal();
}
}}
>
<button onClick={closeModal} className="close-btn">
Close
</button>
{children}
</dialog>
);
}
Default Modal Slot
// app/@modal/default.tsx
export default function DefaultModal() {
return null; // Don't show modal by default
}
Advanced Parallel Routes Patterns
Conditional Slot Rendering
// app/dashboard/layout.tsx
export default async function DashboardLayout({
children,
team,
analytics
}: {
children: React.ReactNode;
team: React.ReactNode;
analytics: React.ReactNode;
}) {
const session = await getSession();
const isAdmin = session?.role === 'admin';
return (
<div className="dashboard">
{children}
{isAdmin ? (
<>
<div className="slot">{team}</div>
<div className="slot">{analytics}</div>
</>
) : (
<div className="slot">{team}</div>
)}
</div>
);
}
Tab-Based Navigation with Parallel Routes
app/
profile/
@tabs/
posts/
page.tsx
followers/
page.tsx
following/
page.tsx
default.tsx
layout.tsx
page.tsx
// app/profile/layout.tsx
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
export default function ProfileLayout({
children,
tabs
}: {
children: React.ReactNode;
tabs: React.ReactNode;
}) {
const pathname = usePathname();
return (
<div>
{children}
<nav className="tabs">
<Link
href="/profile/posts"
className={pathname === '/profile/posts' ? 'active' : ''}
>
Posts
</Link>
<Link
href="/profile/followers"
className={pathname === '/profile/followers' ? 'active' : ''}
>
Followers
</Link>
<Link
href="/profile/following"
className={pathname === '/profile/following' ? 'active' : ''}
>
Following
</Link>
</nav>
<div className="tab-content">{tabs}</div>
</div>
);
}
Combining Parallel and Intercepting Routes
app/
feed/
page.tsx
[id]/
page.tsx # Full post page
@modal/
(.)feed/
[id]/
page.tsx # Intercepted modal
default.tsx
layout.tsx
// app/feed/page.tsx
import Link from 'next/link';
export default function Feed() {
const posts = await getPosts();
return (
<div className="feed">
{posts.map(post => (
<Link key={post.id} href={`/feed/${post.id}`}>
<article>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
</Link>
))}
</div>
);
}
Warning: When using intercepting routes with parallel routes, be careful with route matching. Hard refreshes will always show the non-intercepted route.
Slot-Based Layouts Best Practices
Independent Error Boundaries
// app/dashboard/@analytics/error.tsx
'use client';
export default function AnalyticsError({
error,
reset
}: {
error: Error;
reset: () => void;
}) {
return (
<div className="error-container">
<h2>Analytics Failed to Load</h2>
<p>{error.message}</p>
<button onClick={reset}>Try Again</button>
</div>
);
}
Slot Navigation Utilities
// lib/parallel-routes.ts
export function createSlotUrl(base: string, slots: Record<string, string>) {
const params = new URLSearchParams();
Object.entries(slots).forEach(([key, value]) => {
params.set(key, value);
});
return `${base}?${params.toString()}`;
}
// Usage
const url = createSlotUrl('/dashboard', {
team: 'engineering',
analytics: 'weekly'
});
// Result: /dashboard?team=engineering&analytics=weekly
Exercise: Build a Product Gallery with Modal Preview
Create a product gallery that:
- Displays products in a grid layout
- Opens a modal when clicking a product (intercepting route)
- Allows direct navigation to full product page
- Uses parallel routes for reviews and related products
- Implements independent loading states for each slot
Bonus: Add URL state management so the modal can be shared via link.
Performance Considerations
- Streaming: Parallel routes can stream independently, improving perceived performance
- Code Splitting: Each slot is automatically code-split
- Suspense Boundaries: Wrap slots in Suspense for better loading UX
- Preloading: Use
Link prefetchfor anticipated navigation
Tip: Use parallel routes for dashboard-style layouts where multiple data sources need to load independently. This prevents one slow query from blocking the entire page.