Parallel Routes (@slots)
Render multiple independent pages into named slots of one layout, each with its own loading, error, and navigation state.
Once you have built a few dashboards you hit the same wall: you want two or three regions on screen that each fetch their own data, stream independently, and even navigate on their own. Stuffing that into a single page.tsx gets ugly fast. Parallel routes are the elegant answer.
The mental model
A folder named with an @ prefix is a slot. It does not add a URL segment. Instead, the slot is handed to the sibling layout.tsx as a prop whose name matches the folder. So @team and @analytics become props team and analytics alongside the usual children.
app/
dashboard/
layout.tsx // receives { children, team, analytics }
page.tsx // fills the implicit "children" slot
@team/
page.tsx
@analytics/
page.tsxEach slot is a full sub-tree: it can have its own loading.tsx, error.tsx, and nested routes. They render at the same time and stream independently, so a slow analytics query never blocks the team list.
The one that trips everyone up: default.tsx
Slots keep their own navigation state. On a hard navigation (full reload) to a URL a slot cannot match, Next needs to know what to render there. It looks for default.tsx in the slot. If it is missing, you get a 404 for the whole route. This is the single most common parallel-routes bug, so add a default.tsx to every slot early.
Example
// app/dashboard/layout.tsx
// Slots arrive as props named after their @folder.
export default function DashboardLayout({
children,
team,
analytics,
}: {
children: React.ReactNode;
team: React.ReactNode;
analytics: React.ReactNode;
}) {
return (
<section>
<header>{children}</header>
<div className="grid">
{/* Each slot streams on its own timeline */}
{team}
{analytics}
</div>
</section>
);
}
// app/dashboard/@analytics/default.tsx
// Required: what to show after a hard nav when the slot
// has no matching sub-route. Missing this = surprise 404.
export default function Default() {
return null;
}When to use it
- A dashboard renders an @analytics slot and a @team slot independently so each can have its own loading state.
- A social app shows a notifications panel in @panel while the main feed updates in @feed without either blocking the other.
- A multi-tab interface uses parallel routes so the active tab can stream its data while sibling tabs remain mounted.
More examples
Slot folder structure
Folder names prefixed with @ become named slot props that the parent layout receives alongside children.
app/
└── dashboard/
├── layout.tsx # receives @analytics and @team
├── @analytics/
│ └── page.tsx # /dashboard (analytics slot)
└── @team/
└── page.tsx # /dashboard (team slot)Layout consuming two slots
Each slot is a prop, so the layout positions them independently while they load and error separately.
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
analytics,
team,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
team: React.ReactNode;
}) {
return (
<div className="grid grid-cols-2 gap-4">
<div>{analytics}</div>
<div>{team}</div>
<div className="col-span-2">{children}</div>
</div>
);
}Default.tsx for unmatched slots
default.tsx renders when a slot has no matching page for the current URL, preventing a 404 for sibling navigation.
// app/dashboard/@analytics/default.tsx
export default function AnalyticsDefault() {
return <p>No analytics selected.</p>;
}
Discussion