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

