Parallel Routes
| Since: | Next.js 13(2022) |
|---|
In the Next.js App Router, parallel routes (also called slots) can be defined by prefixing a folder name with @. This mechanism is used when you want to display multiple different route contents simultaneously within the same layout.js, such as showing a main content area alongside a side panel as independent routes arranged on a single page, as in a dashboard.
Syntax
app/
├── layout.js // The parent layout that receives the @slots
├── page.js // → / (the default main content)
├── @team/ // The folder for the "team" slot (does not appear in the URL)
│ ├── page.js // → Content of the team slot when / is accessed
│ └── members/
│ └── page.js // → Content of the team slot when /members is accessed
└── @analytics/ // The folder for the "analytics" slot (does not appear in the URL)
├── page.js // → Content of the analytics slot when / is accessed
└── revenue/
└── page.js // → Content of the analytics slot when /revenue is accessed
Main Use Cases
| Use Case | Description |
|---|---|
| Dashboard split view | Multiple sections such as main content, charts, and notification panels can each be managed as independent routes and displayed on a single screen. |
| Independent loading and error boundaries | Placing loading.js or error.js inside each slot allows for independent loading displays and error handling per slot. |
| Modals coexisting with pages | Parallel routes can be combined with intercepting routes to achieve a UI that displays a modal while changing the URL and maintaining the background page. |
| Conditional rendering | Depending on authentication status or user role, the content of the slot displayed can be switched even for the same URL. |
Sample Code
An example of simultaneously displaying a team member list and sales analytics as separate slots on a dashboard page.
// Directory structure // app/dashboard/ // ├── layout.js ← Layout that receives @team and @analytics // ├── page.js ← Main content for /dashboard // ├── @team/ // │ ├── default.js ← Fallback when no matching page exists for the slot // │ └── page.js ← Team slot for /dashboard // └── @analytics/ // ├── default.js ← Fallback when no matching page exists for the slot // └── page.js ← Analytics slot for /dashboard
// app/dashboard/layout.js
// A layout that receives two slots: @team and @analytics
// The slot name becomes the property name (the folder name without @)
export default function DashboardLayout({ children, team, analytics }) {
return (
<div className="dashboard">
{/* The main content area (contains the content of page.js) */}
<main className="dashboard-main">
{children}
</main>
{/* The sidebar area. Displays the two slots stacked vertically. */}
<aside className="dashboard-sidebar">
{/* The content of the @team slot is inserted here */}
<section className="sidebar-team">
{team}
</section>
{/* The content of the @analytics slot is inserted here */}
<section className="sidebar-analytics">
{analytics}
</section>
</aside>
</div>
);
}
// app/dashboard/page.js
// The main content when /dashboard is accessed
export const metadata = {
title: 'Dashboard | My App',
};
export default function DashboardPage() {
return (
<div>
<h1>Dashboard</h1>
<p>You can check team information and sales analytics in the sidebar on the right.</p>
</div>
);
}
// app/dashboard/@team/page.js
// The component displayed in the team slot when /dashboard is accessed
// This component is inserted at the {team} position in layout.js
export default async function TeamSlot() {
// Fetch the list of team members from the API
const res = await fetch('https://api.example.com/team', {
// Revalidate the cache every 60 seconds
next: { revalidate: 60 },
});
const members = await res.json();
return (
<div>
<h2>Team Members</h2>
<ul>
{/* Display the member list as a bulleted list */}
{members.map(function(member) {
return (
<li key={member.id}>
{member.name} ({member.role})
</li>
);
})}
</ul>
</div>
);
}
// app/dashboard/@analytics/page.js
// The component displayed in the analytics slot when /dashboard is accessed
// This component is inserted at the {analytics} position in layout.js
export default async function AnalyticsSlot() {
// Fetch sales data from the API
const res = await fetch('https://api.example.com/analytics/summary', {
// Fetch the latest data every time without caching
cache: 'no-store',
});
const data = await res.json();
return (
<div>
<h2>Sales Summary</h2>
{/* Display key metrics in a table */}
<table>
<tbody>
<tr>
<th>This Month's Sales</th>
<td>{data.monthlySales.toLocaleString()}</td>
</tr>
<tr>
<th>Month-over-Month</th>
<td>{data.growthRate}%</td>
</tr>
</tbody>
</table>
</div>
);
}
// app/dashboard/@team/default.js
// The fallback component displayed when no page matching the slot
// is found after navigation.
// Without this file, unmatched transitions result in a 404 error.
export default function TeamDefault() {
return (
<div>
<p>Loading team information...</p>
</div>
);
}
Common Mistakes
Common Mistake 1: Forgetting to place default.js, causing the slot to produce a 404 error
When navigation occurs within a layout that uses parallel routes, there are cases where no page.js exists for the destination URL within a slot. In that case, Next.js looks for the slot's default.js. If default.js does not exist, the result is a 404 error. For example, when navigating from /dashboard to /dashboard/settings, if there is no settings/page.js in the @team slot, an @team/default.js is required.
ng_example.jsx
// Directory structure (NG example)
// Contains slots without default.js
app/dashboard/
├── layout.js
├── page.js
├── settings/
│ └── page.js // Main content for /dashboard/settings
├── @team/
│ └── page.js // Team slot for /dashboard
│ // default.js is missing
└── @analytics/
└── page.js // Analytics slot for /dashboard
// default.js is missing
Navigating from /dashboard to /dashboard/settings results in a 404 error because the @team and @analytics slots have no matching page.
Place a default.js in each slot folder. default.js is displayed as a fallback when no matching page exists during navigation.
ok_example.jsx
// Directory structure (OK example)
// Each slot has a default.js
app/dashboard/
├── layout.js
├── page.js
├── settings/
│ └── page.js
├── @team/
│ ├── page.js
│ └── default.js // Fallback component
└── @analytics/
├── page.js
└── default.js // Fallback component
Common Mistake 2: Misspelling the slot name in the layout props, causing null to be passed
The slot's folder name (without the @) becomes the property name in the parent layout's props. If the property name used in the layout does not match the folder name, the slot's content becomes null or undefined and nothing is displayed. Spelling mistakes are easy to overlook, so careful attention is needed.
ng_example.jsx
// app/dashboard/layout.js
// The slot name spelling does not match the actual folder name
// Folder name: @analytics
// But "analytic" (missing the "s") is written here
export default function DashboardLayout({ children, team, analytic }) {
return (
<div>
<main>{children}</main>
<aside>
{team}
{/* analytic is undefined, so nothing is displayed */}
{analytic}
</aside>
</div>
);
}
The content of the @analytics slot does not appear on the screen. (analytic is undefined, so nothing is rendered.)
Verify that the folder name (@analytics) and the layout property name (analytics) match exactly.
ok_example.jsx
// app/dashboard/layout.js
// Received as "analytics" to match the folder name @analytics
export default function DashboardLayout({ children, team, analytics }) {
return (
<div>
<main>{children}</main>
<aside>
{team}
{/* analytics is correctly passed and the content is displayed */}
{analytics}
</aside>
</div>
);
}
Overview
Parallel routes are enabled simply by creating a folder with the @folder-name naming convention. These folders are called "slots," and the folder name (without @) is passed as a property name to the parent layout. Slot names are not included in the URL path segments, so app/dashboard/@team/page.js is displayed when accessing /dashboard, not /dashboard/team.
Since each slot is treated as an independent route, placing loading.js or error.js inside the slot folder allows you to configure individual loading displays and error handling per slot. For example, you can display a skeleton UI only for a slot whose data fetch takes a long time while other slots render first.
One important point to note: when navigating within a layout that uses parallel routes, there are cases where a corresponding page.js does not exist for a given URL in each slot. In this case, Next.js recursively searches the slot folder for a default.js to display as a fallback. If no default.js is provided, the result is a 404 error, so it is common practice to place a default.js in each slot.
For how to define common layouts, see layout.js. For grouping routes, see Route Groups.
If you find any errors or copyright issues, please contact us.