Parallel Routes
Parallel routes allow you to simultaneously render multiple pages in the same layout. They're defined using the @folder convention.
The Concept
Instead of one children slot, layouts can have multiple named slots:
app/
āāā layout.tsx
āāā page.tsx
āāā @analytics/
ā āāā page.tsx
āāā @team/
āāā page.tsx
// app/layout.tsx
export default function Layout({
children,
analytics,
team,
}: {
children: React.ReactNode
analytics: React.ReactNode
team: React.ReactNode
}) {
return (
<div>
<main>{children}</main>
<aside>
{analytics}
{team}
</aside>
</div>
)
}
Common Use Cases
Dashboard with Multiple Panels
app/dashboard/
āāā layout.tsx
āāā page.tsx ā Main content
āāā @stats/
ā āāā page.tsx ā Stats panel
āāā @notifications/
ā āāā page.tsx ā Notifications panel
āāā @activity/
āāā page.tsx ā Activity feed
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
stats,
notifications,
activity,
}: {
children: React.ReactNode
stats: React.ReactNode
notifications: React.ReactNode
activity: React.ReactNode
}) {
return (
<div className="grid grid-cols-4 gap-4">
<main className="col-span-3">{children}</main>
<aside className="space-y-4">
{stats}
{notifications}
{activity}
</aside>
</div>
)
}
Independent Loading States
Each slot can have its own loading UI:
app/dashboard/
āāā @stats/
ā āāā page.tsx
ā āāā loading.tsx ā Stats skeleton
āāā @activity/
ā āāā page.tsx
ā āāā loading.tsx ā Activity skeleton
Slots load independently - fast panels appear immediately.
Conditional Rendering
Show different content based on state:
// app/layout.tsx
import { getUser } from '@/lib/auth'
export default async function Layout({
children,
auth,
dashboard,
}: {
children: React.ReactNode
auth: React.ReactNode
dashboard: React.ReactNode
}) {
const user = await getUser()
return (
<div>
{user ? dashboard : auth}
{children}
</div>
)
}
Default.tsx for Unmatched Routes
When navigating between routes, slots need a fallback:
app/
āāā layout.tsx
āāā page.tsx ā /
āāā @modal/
ā āāā default.tsx ā Shows when no modal route matches
ā āāā (.)photos/[id]/
ā āāā page.tsx ā Shows for /photos/123
āāā photos/[id]/
āāā page.tsx
// app/@modal/default.tsx
export default function Default() {
return null // Don't render anything when no modal
}
Modal Pattern with Parallel Routes
The most common use case - modals that can be linked directly:
app/
āāā layout.tsx
āāā page.tsx
āāā @modal/
ā āāā default.tsx
ā āāā (.)photos/[id]/
ā āāā page.tsx ā Modal version
āāā photos/
āāā [id]/
āāā page.tsx ā Full page version
- Click a photo link: Opens modal (via intercepting route)
- Direct link to
/photos/123: Full page - Refresh on modal: Falls back to full page
Independent Error Handling
Each slot can have its own error boundary:
app/dashboard/
āāā @stats/
ā āāā page.tsx
ā āāā error.tsx ā Only catches stats errors
āāā @activity/
ā āāā page.tsx
ā āāā error.tsx ā Only catches activity errors
If stats fails, activity still works.
Summary
- Parallel routes use
@folderconvention - Layout receives slots as props alongside
children - Each slot can have independent loading/error states
- Use
default.tsxfor unmatched slot routes - Great for dashboards, modals, and conditional layouts
- Slots render simultaneously, not sequentially

