Parallel Routes in Next.js 16
Concept
Parallel routes allow you to simultaneously render multiple pages within the same layout. Each route is defined in a "slot" using the @folder naming convention.
Key characteristics:
- •Slots are defined with
@prefix (e.g.,@team,@analytics) - •Each slot is passed as a prop to the parent layout
- •Slots render independently and can have their own loading/error states
- •Navigation within one slot doesn't affect other slots
Basic Structure
app/ ├── @team/ │ ├── page.tsx │ └── default.tsx ├── @analytics/ │ ├── page.tsx │ └── default.tsx ├── layout.tsx └── page.tsx
The layout receives slots as props:
export default function Layout({
children,
team,
analytics,
}: {
children: React.ReactNode
team: React.ReactNode
analytics: React.ReactNode
}) {
return (
<div>
<div>{children}</div>
<div className="grid grid-cols-2 gap-4">
<div>{team}</div>
<div>{analytics}</div>
</div>
</div>
)
}
default.tsx Requirement
CRITICAL: Every slot MUST have a default.tsx file.
When navigating to a route that doesn't match the current slot, Next.js will render the default.tsx fallback. Without it, you'll get a 404 error.
export default function Default() {
return null
}
Common error:
Error: The default export is not a React Component in route: /@team
Solution: Add default.tsx to every @slot directory.
Navigation Behavior
Parallel routes handle navigation differently than normal routes:
Soft Navigation
When navigating using <Link> or router.push():
- •Active slots maintain their current state
- •Only the children segment updates
- •Slots remain "mounted"
Hard Navigation
When navigating via browser refresh or direct URL entry:
- •All slots reset to their default state
- •
default.tsxfiles render for unmatched routes - •Each slot independently resolves its route
Example scenario:
app/ ├── @modal/ │ ├── login/ │ │ └── page.tsx │ └── default.tsx ├── layout.tsx └── page.tsx
- •User visits
/→@modalrendersdefault.tsx - •User clicks link to
/login→@modalrenderslogin/page.tsx - •User refreshes page → URL changes back to
/,@modalrendersdefault.tsx
This is why intercepting routes typically use parallel routes for modals.
Slot Matching
Slots match based on the current URL segment:
app/ ├── dashboard/ │ ├── @sidebar/ │ │ ├── settings/ │ │ │ └── page.tsx │ │ └── default.tsx │ ├── settings/ │ │ └── page.tsx │ └── layout.tsx └── page.tsx
At /dashboard/settings:
- •
dashboard/settings/page.tsxrenders inchildren - •
dashboard/@sidebar/settings/page.tsxrenders insidebarslot - •If
@sidebar/settings/page.tsxdoesn't exist,@sidebar/default.tsxrenders
Conditional Rendering
You can conditionally render slots:
export default function Layout({
children,
modal,
auth,
}: {
children: React.ReactNode
modal: React.ReactNode
auth: React.ReactNode
}) {
const session = await getSession()
return (
<div>
{children}
{modal}
{!session && auth}
</div>
)
}
Common Gotchas
1. Missing default.tsx
Problem: 404 errors when navigating
Solution: Add default.tsx to every slot directory
2. Slot not rendering
Problem: Slot prop is undefined in layout
Solution: Check slot name matches @folder name (case-sensitive)
3. Unexpected resets
Problem: Slot resets to default on navigation
Solution: Ensure target route exists in slot, or use default.tsx intentionally
4. Nested layouts
Problem: Slots not accessible in nested layouts Solution: Slots only pass to immediate parent layout, not descendants
5. Route groups with slots
Problem: Confusion about where to place @slot folders
Solution: Place slots at the same level as the layout that consumes them
app/ ├── (marketing)/ │ ├── @hero/ │ │ └── page.tsx │ ├── layout.tsx │ └── page.tsx
Practical Example: Modal Pattern
app/ ├── @modal/ │ ├── (.)photo/ │ │ └── [id]/ │ │ └── page.tsx │ └── default.tsx ├── photo/ │ └── [id]/ │ └── page.tsx ├── layout.tsx └── page.tsx
Layout with modal slot:
export default function Layout({
children,
modal,
}: {
children: React.ReactNode
modal: React.ReactNode
}) {
return (
<>
{children}
{modal}
</>
)
}
Modal slot default:
export default function Default() {
return null
}
Intercepted route renders in modal:
export default function PhotoModal({ params }: { params: { id: string } }) {
return (
<dialog open>
<img src={`/photos/${params.id}.jpg`} />
</dialog>
)
}
Direct route renders full page:
export default function PhotoPage({ params }: { params: { id: string } }) {
return (
<main>
<img src={`/photos/${params.id}.jpg`} />
</main>
)
}
When to Use Parallel Routes
Good use cases:
- •Dashboard layouts with independent panels
- •Modal/drawer patterns with intercepting routes
- •Split views with independent navigation
- •A/B testing different UI sections
- •Conditional sidebar/navigation rendering
Avoid when:
- •Simple component composition suffices
- •You need parent-child data flow
- •Navigation should be tightly coupled
- •You're just trying to organize files (use route groups instead)
References
- •Next.js Docs: https://nextjs.org/docs/app/building-your-application/routing/parallel-routes
- •Intercepting Routes: nextjs-16 skill
ROUTING-intercepting-routes - •Route Groups: nextjs-16 skill
ROUTING-route-groups