Parallel Routes let you render multiple pages in the same layout simultaneously, allowing for things like modals or sidebars that don’t interrupt the main page flow.
Let’s see it in action. Imagine a dashboard with a primary content area and a secondary sidebar. We want to be able to open a settings modal over the dashboard without navigating away from the dashboard itself.
Here’s a simplified app/dashboard/layout.tsx:
export default function DashboardLayout({
children,
settings,
}: {
children: React.ReactNode;
settings: React.ReactNode;
}) {
return (
<div>
<nav>Dashboard Navigation</nav>
<main>{children}</main>
<aside>{settings}</aside>
</div>
);
}
Notice the settings prop. This isn’t a standard route segment; it’s a slot for a parallel route.
Now, let’s define that parallel route in app/dashboard/parallel/@settings/page.tsx:
export default function SettingsPage() {
return (
<div>
<h2>Settings</h2>
<p>Configure your dashboard here.</p>
</div>
);
}
The @settings directory signifies a parallel route. When you navigate to /dashboard, Next.js will render both children (the default content of /dashboard) and the content of @settings.
But what if we only want the settings to appear conditionally, like in a modal? That’s where Intercepting Routes come in. They allow you to "intercept" a route and render different UI instead, often for modals or drawers.
Let’s modify our layout to conditionally render the settings. We’ll use an intercepting route to control its display. First, create app/dashboard/@settings/modal.tsx. The modal.tsx filename is key here, as it tells Next.js this is an intercepting route for a modal.
// app/dashboard/@settings/modal.tsx
export default function SettingsModal() {
return (
<div className="modal-backdrop">
<div className="modal-content">
<SettingsPage /> {/* Render the actual settings page content */}
</div>
</div>
);
}
Now, the magic happens in the app/dashboard/layout.tsx. We’ll change it to:
import SettingsPage from './parallel/@settings/page'; // Import the actual page
export default function DashboardLayout({
children,
settings,
}: {
children: React.ReactNode;
settings: React.ReactNode; // This will now be the intercepted modal route
}) {
return (
<div>
<nav>Dashboard Navigation</nav>
<main>{children}</main>
{settings} {/* Render the intercepted route content */}
</div>
);
}
And critically, we need to tell Next.js how to intercept. Create app/dashboard/loading.tsx (or error.tsx or template.tsx) to indicate that this slot is being intercepted. The presence of this file in the @settings directory (or a parent directory) signals the interception.
Let’s refine this by actually intercepting the route from a link click. Suppose you have a link on your dashboard that should open the settings modal.
In app/dashboard/page.tsx:
import Link from 'next/link';
export default function DashboardPage() {
return (
<div>
<h1>Dashboard</h1>
<Link href="/dashboard/settings">Open Settings</Link> {/* This link triggers the intercept */}
<p>Main content area.</p>
</div>
);
}
Now, create the intercepting route at app/(dashboard)/settings/page.tsx. Notice the (dashboard) group. This is crucial for routing. The actual parallel route slot is still defined in the layout.
// app/(dashboard)/settings/page.tsx
import SettingsModal from '../@settings/modal'; // Import the modal component
export default function InterceptedSettingsPage() {
return <SettingsModal />;
}
When you click the "Open Settings" link, Next.js sees that /dashboard/settings matches a route and that there’s an intercepting route defined in app/(dashboard)/settings/page.tsx which renders the SettingsModal. This modal is then slotted into the @settings outlet in app/dashboard/layout.tsx.
The key insight is that parallel routes define slots in your UI, and intercepting routes define how to fill those slots when a specific URL is matched, often without changing the primary URL of the parent layout. The URL does change to /dashboard/settings, but the layout remains /dashboard, and the content of /dashboard/page.tsx is still visible behind the modal.
The mechanism at play is that Next.js analyzes the URL and the file structure. When you navigate to /dashboard/settings, it matches the intercepting route. It then looks at the closest layout that has a parallel route slot named @settings (in this case, app/dashboard/layout.tsx) and renders the output of the intercepting route into that slot. If the intercepting route were to render a full page component, it would overwrite the children slot. But by rendering a modal component that uses the actual settings page content, we achieve the overlay effect.
The true power comes when you combine these. You can have multiple parallel routes, each potentially intercepted by different modal or drawer UIs, all managed within a single layout. This allows for highly dynamic and interactive UIs without the complexity of client-side routing hacks.
The most surprising mechanical detail is that the intercepting route’s URL (e.g., /dashboard/settings) becomes part of the browser’s history and is reflected in the URL bar, even though the UI rendered is within a parallel slot of a different, parent route’s layout. This gives you bookmarkable and shareable states for your overlaid UIs.
The next concept you’ll encounter is how to manage state and interactions between these parallel routes and the main content.