Most routes render a single page at a time. But what if you need to display multiple independent sections simultaneouslyβlike a dashboard with analytics, team activity, and notifications all updating independently? That's where parallel routes shine. Using the @folder syntax, you can create "slots" that render different pages in the same layout, each with its own loading states, errors, and navigation. This is an advanced pattern, but incredibly powerful once you master it.
What Are Parallel Routes?
Parallel routes allow you to simultaneously render multiple pages in the same layout. Think of them as "slots" or "panels" that can each display different content independently.
The Problem They Solve
Imagine building a dashboard that shows:
- π Analytics charts
- π₯ Team activity feed
- π Notifications panel
- π Recent sales
With traditional routing, you'd have to:
- Fetch all data in one page component (messy!)
- Create separate components and import them (loses routing benefits)
- Use client-side state management (complex!)
Parallel routes let each section be its own route with independent:
- Loading states
- Error boundaries
- Data fetching
- URL-based navigation
Key Benefits
- Independent loading: Each section can load at its own pace
- Separate error handling: One section failing doesn't break others
- URL-driven: Each section can respond to URL changes
- Better organization: Clear separation of concerns
The @ Syntax: Defining Slots
Parallel routes use folders prefixed with @ to define slots:
app/
dashboard/
layout.tsx β Receives all slots as props
page.tsx β Main content (optional)
@analytics/ β Slot named "analytics"
page.tsx
@team/ β Slot named "team"
page.tsxThe @ prefix tells Next.js: "This is a slot, not a URL segment."
Basic Parallel Routes Structure
Two slots (@analytics and @team) rendering simultaneously
Select a file or folder to see details
π Slots vs URL Segments
Like route groups (folder), slots @folder do not appear in URLs. They're purely for organization and parallel rendering.
Creating Your First Parallel Routes
Let's build a dashboard with two parallel sections step by step:
Step 1: Create the Folder Structure
app/
dashboard/
layout.tsx
@analytics/
page.tsx
@team/
page.tsxStep 2: Create the Slot Pages
// This renders in the analytics slot
export default function AnalyticsSlot() {
return (
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-xl font-bold mb-4">Analytics</h2>
<div className="space-y-4">
<div className="flex items-center justify-between">
<span className="text-gray-600">Total Users</span>
<span className="text-2xl font-bold">1,234</span>
</div>
<div className="flex items-center justify-between">
<span className="text-gray-600">Revenue</span>
<span className="text-2xl font-bold">$45,678</span>
</div>
<div className="flex items-center justify-between">
<span className="text-gray-600">Active Sessions</span>
<span className="text-2xl font-bold">89</span>
</div>
</div>
</div>
);
}// This renders in the team slot
export default function TeamSlot() {
const teamMembers = [
{ name: 'Alice Johnson', status: 'online', avatar: 'π©' },
{ name: 'Bob Smith', status: 'away', avatar: 'π¨' },
{ name: 'Carol Davis', status: 'online', avatar: 'π©' },
];
return (
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-xl font-bold mb-4">Team Activity</h2>
<div className="space-y-3">
{teamMembers.map((member) => (
<div key={member.name} className="flex items-center gap-3">
<div className="text-3xl">{member.avatar}</div>
<div className="flex-1">
<div className="font-semibold">{member.name}</div>
<div className="text-sm text-gray-500">{member.status}</div>
</div>
<div
className={`w-2 h-2 rounded-full ${
member.status === 'online' ? 'bg-green-500' : 'bg-gray-400'
}`}
/>
</div>
))}
</div>
</div>
);
}Step 3: Compose Slots in the Layout
// Layout receives each slot as a prop with the same name
export default function DashboardLayout({
children,
analytics, // @analytics slot
team, // @team slot
}: {
children: React.ReactNode;
analytics: React.ReactNode;
team: React.ReactNode;
}) {
return (
<div className="min-h-screen bg-gray-50 p-8">
<h1 className="text-3xl font-bold mb-8">Dashboard</h1>
{/* Grid layout with slots */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Analytics slot */}
<div>{analytics}</div>
{/* Team slot */}
<div>{team}</div>
</div>
{/* Main content (if page.tsx exists) */}
{children && (
<div className="mt-6">
{children}
</div>
)}
</div>
);
}How Props Work
Each slot becomes a prop in the layout with the slot name (without @):
@analyticsβanalyticsprop@teamβteamprop@notificationsβnotificationsprop
β¨ It Just Works!
Visit /dashboard and you'll see both panels rendering simultaneously! Each is independently fetching data and rendering its own UI.
Default Fallbacks with default.tsx
What happens when a slot doesn't have a matching route? That's where default.tsx comes in:
app/
dashboard/
@analytics/
page.tsx β Matches /dashboard
revenue/
page.tsx β Matches /dashboard/revenue
default.tsx β Fallback for unmatched routes// This renders when the slot doesn't match
export default function AnalyticsDefault() {
return (
<div className="bg-gray-100 rounded-lg p-6 text-center">
<p className="text-gray-600">No analytics data available for this view</p>
</div>
);
}When default.tsx is Used
Consider this structure:
app/
dashboard/
@analytics/
page.tsx β Has /dashboard
revenue/
page.tsx β Has /dashboard/revenue
default.tsx
settings/
page.tsx β /dashboard/settings existsWhat renders at different URLs:
/dashboardβ @analytics/page.tsx β/dashboard/revenueβ @analytics/revenue/page.tsx β/dashboard/settingsβ @analytics/default.tsx (no analytics/settings)
Important: Always Provide default.tsx
Without default.tsx, navigating to a route without a matching slot will show a 404 for that slot. Always include defaults for better UX!
Independent Navigation in Slots
Each slot can have its own navigation structure:
app/
dashboard/
layout.tsx
@analytics/
page.tsx β /dashboard
revenue/
page.tsx β /dashboard/revenue
users/
page.tsx β /dashboard/users
default.tsx
@team/
page.tsx β /dashboard
members/
page.tsx β /dashboard/members
projects/
page.tsx β /dashboard/projects
default.tsxNow you can navigate to:
/dashboard- Both slots show their main pages/dashboard/revenue- Analytics shows revenue, team shows default/dashboard/members- Team shows members, analytics shows default
Adding Navigation Links
import Link from 'next/link';
export default function AnalyticsSlot() {
return (
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-xl font-bold mb-4">Analytics</h2>
{/* Navigation within this slot */}
<nav className="flex gap-4 mb-6 text-sm">
<Link
href="/dashboard"
className="text-blue-600 hover:underline"
>
Overview
</Link>
<Link
href="/dashboard/revenue"
className="text-blue-600 hover:underline"
>
Revenue
</Link>
<Link
href="/dashboard/users"
className="text-blue-600 hover:underline"
>
Users
</Link>
</nav>
{/* Content */}
<div>Overview analytics data...</div>
</div>
);
}Real-World Example: Advanced Dashboard
Let's build a comprehensive dashboard with multiple parallel sections:
Advanced Dashboard with 3 Parallel Slots
Analytics, notifications, and activity feed running independently
Select a file or folder to see details
The Layout: Composing Multiple Slots
export default function DashboardLayout({
children,
analytics,
notifications,
activity,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
notifications: React.ReactNode;
activity: React.ReactNode;
}) {
return (
<div className="min-h-screen bg-gray-50">
{/* Header */}
<header className="bg-white border-b px-8 py-4">
<h1 className="text-2xl font-bold">Dashboard</h1>
</header>
<div className="p-8">
{/* Top row: Analytics (wide) */}
<div className="mb-6">
{analytics}
</div>
{/* Bottom row: Notifications and Activity side by side */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div>{notifications}</div>
<div>{activity}</div>
</div>
{/* Main content area (optional) */}
{children && (
<div className="mt-6">
{children}
</div>
)}
</div>
</div>
);
}Individual Slot Components
import Link from 'next/link';
export default async function AnalyticsSlot() {
// Fetch analytics data (Server Component!)
const stats = await fetch('https://api.example.com/stats')
.then(r => r.json());
return (
<div className="bg-white rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-bold">Analytics</h2>
<nav className="flex gap-4 text-sm">
<Link href="/dashboard" className="text-blue-600">
Overview
</Link>
<Link href="/dashboard/revenue" className="text-blue-600">
Revenue
</Link>
<Link href="/dashboard/users" className="text-blue-600">
Users
</Link>
</nav>
</div>
<div className="grid grid-cols-3 gap-4">
<div className="text-center p-4 bg-blue-50 rounded">
<div className="text-3xl font-bold text-blue-600">
{stats.users}
</div>
<div className="text-sm text-gray-600">Total Users</div>
</div>
<div className="text-center p-4 bg-green-50 rounded">
<div className="text-3xl font-bold text-green-600">
${stats.revenue}
</div>
<div className="text-sm text-gray-600">Revenue</div>
</div>
<div className="text-center p-4 bg-purple-50 rounded">
<div className="text-3xl font-bold text-purple-600">
{stats.sessions}
</div>
<div className="text-sm text-gray-600">Active Sessions</div>
</div>
</div>
</div>
);
}export default async function NotificationsSlot() {
// Independent data fetching
const notifications = await fetch('https://api.example.com/notifications')
.then(r => r.json());
return (
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-xl font-bold mb-4">Notifications</h2>
<div className="space-y-3">
{notifications.map((notif: any) => (
<div
key={notif.id}
className="flex items-start gap-3 p-3 bg-gray-50 rounded"
>
<div className="text-2xl">{notif.icon}</div>
<div className="flex-1">
<div className="font-semibold text-sm">{notif.title}</div>
<div className="text-xs text-gray-600">{notif.message}</div>
<div className="text-xs text-gray-400 mt-1">{notif.time}</div>
</div>
</div>
))}
</div>
</div>
);
}export default async function ActivitySlot() {
// Yet another independent fetch
const activities = await fetch('https://api.example.com/activity')
.then(r => r.json());
return (
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-xl font-bold mb-4">Recent Activity</h2>
<div className="space-y-3">
{activities.map((activity: any) => (
<div key={activity.id} className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center">
{activity.user.charAt(0)}
</div>
<div className="flex-1">
<div className="text-sm">
<span className="font-semibold">{activity.user}</span>
{' '}{activity.action}
</div>
<div className="text-xs text-gray-500">{activity.time}</div>
</div>
</div>
))}
</div>
</div>
);
}π― Key Advantages
Notice how each slot:
- Fetches its own data independently
- Can have its own loading states
- Handles its own errors
- Updates without affecting others
Loading and Error States for Slots
Each slot can have its own loading.tsx and error.tsx:
app/
dashboard/
@analytics/
page.tsx
loading.tsx β Shows while analytics loads
error.tsx β Shows if analytics errors
default.tsxexport default function AnalyticsLoading() {
return (
<div className="bg-white rounded-lg shadow p-6">
<div className="animate-pulse">
<div className="h-6 bg-gray-200 rounded w-1/4 mb-4" />
<div className="space-y-3">
<div className="h-20 bg-gray-200 rounded" />
<div className="h-20 bg-gray-200 rounded" />
<div className="h-20 bg-gray-200 rounded" />
</div>
</div>
</div>
);
}'use client';
export default function AnalyticsError({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-xl font-bold text-red-600 mb-2">
Analytics Error
</h2>
<p className="text-gray-600 mb-4">
Failed to load analytics data
</p>
<button
onClick={reset}
className="px-4 py-2 bg-blue-600 text-white rounded"
>
Try Again
</button>
</div>
);
}Now if analytics fails to load, only that panel shows an errorβthe rest of the dashboard continues working!
Common Use Cases for Parallel Routes
1. Dashboard with Multiple Panels
app/
dashboard/
layout.tsx
@metrics/page.tsx β KPI metrics
@charts/page.tsx β Data visualizations
@activity/page.tsx β Activity feed
@team/page.tsx β Team status2. Split View Editor
app/
editor/
layout.tsx
@code/page.tsx β Code editor
@preview/page.tsx β Live preview
@console/page.tsx β Console output3. E-commerce Product Page
app/
products/
[id]/
layout.tsx
@details/page.tsx β Product details
@reviews/page.tsx β Customer reviews
@recommended/page.tsx β Recommended products4. Multi-Tenant Admin
app/
admin/
layout.tsx
@tenant-a/page.tsx β Tenant A data
@tenant-b/page.tsx β Tenant B data
@analytics/page.tsx β Combined analyticsParallel Routes Best Practices
1. Always Provide default.tsx
Create fallbacks to handle navigation gracefully:
// @slot/default.tsx
export default function SlotDefault() {
return (
<div className="p-6 text-center text-gray-500">
No content available for this view
</div>
);
}2. Keep Slots Focused
Each slot should represent one logical section:
- β @analytics, @notifications, @activity
- β @section1, @panel2, @stuff
3. Use Consistent Naming
Name slots after what they display, not where they appear:
- β @revenue, @userStats, @recentOrders
- β @leftPanel, @rightSide, @topBar
4. Consider Performance
Each slot fetches independently, which is powerful but can impact performance:
// Good: Parallel fetching (fast!)
export default async function Layout({ analytics, team, activity }) {
// All three slots fetch in parallel
return <div>...</div>;
}
// Consider: If you have 10 slots, that's 10 parallel fetches
// Make sure your API can handle it!5. Provide Visual Feedback
Use loading.tsx to show skeleton screens:
// @analytics/loading.tsx
export default function Loading() {
return (
<div className="animate-pulse space-y-4">
<div className="h-4 bg-gray-200 rounded w-3/4" />
<div className="h-32 bg-gray-200 rounded" />
</div>
);
}Limitations and Gotchas
1. Slot Content Doesn't Affect URL
Slots render based on the URL, but navigating within a slot doesn't change the main URL unless you use full paths.
2. All Slots Must Match or Have Defaults
If the URL is /dashboard/settings and @analytics doesn't have a settings route or default.tsx, you'll get a 404 for that slot.
3. Complexity Can Grow Quickly
With many slots, your folder structure becomes complex. Document your architecture well!
4. Not Always Necessary
For simple layouts, regular components imported in page.tsx might be simpler. Use parallel routes when you need independent routing, loading, and error states.
When to Use Parallel Routes
β Use Parallel Routes When:
- Building complex dashboards with independent sections
- Each section needs its own loading/error states
- Sections should update independently based on URL
- You want split views that navigate independently
- Building multi-panel interfaces
β Don't Use Parallel Routes When:
- Simple component composition works fine
- All sections share the same data
- You don't need independent navigation
- The complexity outweighs the benefits
Practice: Build a Dashboard
Dashboard Layout with Parallel Routes
Layout composing two parallel slots
Output Preview
π― Challenge
Create this structure in your project:
- Dashboard with
@analyticsand@teamslots - Each slot with its own page.tsx and default.tsx
- Add loading.tsx to see independent loading states
- Navigate to see how slots update independently
Key Takeaways
- Parallel routes use @folder syntax - creates named slots
- Slots become props in layouts - compose them however you want
- Each slot is independent - own loading, errors, data fetching
- Always provide default.tsx - graceful fallbacks
- Perfect for dashboards and split views - complex UI patterns
- Slots don't affect URLs - like route groups
- Can navigate within slots - independent routing
- Use when complexity is justified - not for everything
What's Next?
Parallel routes are powerful but complex. You've just learned one of Next.js's most advanced routing features! In the next lesson, we'll explore intercepting routesβanother advanced pattern for showing modals and overlays while preserving URL navigation.
Intercepting routes let you "intercept" navigation to show different content (like a modal) while maintaining the ability to refresh or share the URL. It's the final piece of Next.js's advanced routing puzzle!
π You're Mastering Advanced Patterns!
Parallel routes represent advanced Next.js architecture. If you understand them, you're well on your way to building sophisticated applications. Don't worry if it feels complexβthese patterns become clearer with practice!