EmpireUI
Get Pro
← Blog8 min read#next-js#app-router#parallel-routes

Next.js Parallel Routes: Slots, Modals, and Intercepting

Next.js parallel routes let you render multiple pages in one layout using slots — modals, drawers, and split views without the usual state hacks. Here's how they actually work.

Code editor showing Next.js file structure with parallel route folders

What Are Parallel Routes and Why Should You Care

Honestly, parallel routes are one of those Next.js App Router features that sounds abstract until you hit the exact problem they solve — then it clicks instantly. The idea is simple: instead of one page rendering one thing, you can render multiple independently controlled UI segments inside a single layout. At the same time.

The mechanism is slots. You create folders prefixed with @ inside your app directory — like @modal, @sidebar, or @feed. Each slot maps to a prop in your layout's function signature. Your layout then decides where each slot renders in the tree. Each slot can have its own loading state, error boundary, and page hierarchy.

This isn't just a neat trick. It solves real problems. Think about a photo gallery where clicking an image should open a modal *and* update the URL so you can share that link — without navigating away from the gallery. Or a dashboard with a main feed and a persistent side panel that each navigate independently. That's what parallel routes are built for.

Setting Up Your First Slot with @folder Convention

The file system setup is where most people trip up the first time. Slots live at the same directory level as the segments they're parallel to. So if you have app/dashboard/layout.tsx, your slots would be app/dashboard/@analytics/page.tsx and app/dashboard/@team/page.tsx. Not inside each other — side by side.

Here's a concrete layout that renders two slots simultaneously:

// 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-[1fr_320px] gap-6 p-6">
      <main className="min-h-screen">{children}</main>
      <aside className="flex flex-col gap-4">
        {analytics}
        {team}
      </aside>
    </div>
  )
}

Each slot is just a React node prop. You compose them however you want. The children prop is itself the default slot — you don't need to create a @children folder, it's implicit. What's genuinely useful here is that Next.js handles each slot's loading and error states independently. If @analytics is slow, it doesn't block @team from rendering.

default.tsx Files and the Active State Problem

Here's where developers usually get burned. When you navigate to a route that only activates one slot, what does Next.js render in the other slots? It tries to find a match for the current URL in each slot's folder. If it can't — which is often — it falls back to default.tsx inside that slot.

Without a default.tsx, Next.js throws a 404. Not a subtle bug. The whole page errors. So the rule is simple: every slot that could be in an 'unmatched' state during navigation needs a default.tsx. Usually that file just returns null or a skeleton placeholder.

// app/dashboard/@modal/default.tsx
export default function ModalDefault() {
  return null
}

Think of default.tsx as your fallback contract. It's what renders when the slot has no active match for the current URL. Get in the habit of creating it immediately whenever you add a new @slot folder. You'll save yourself a lot of confusing errors during development.

Intercepting Routes to Open Modals Without Losing Context

Intercepting routes are the companion feature to parallel routes, and together they unlock the pattern you've probably seen on Instagram or Pinterest — clicking a photo opens a modal with the photo's URL, but if you hard refresh that URL, you get the full photo page instead. It's two different UIs for the same URL depending on how you arrived.

The folder naming uses parentheses with dots to indicate how many levels up to intercept. (.) intercepts at the same level, (..) intercepts one level up, (..)(..) goes two levels up, and (...) intercepts from the root. It's a bit odd-looking in the file system but logical once you map it to your route hierarchy.

app/
  gallery/
    page.tsx                    ← the full gallery page
    @modal/
      default.tsx               ← renders null normally
      (.)photo/
        [id]/
          page.tsx              ← renders the MODAL version
    photo/
      [id]/
        page.tsx                ← renders the FULL photo page

When a user clicks a photo link from inside the gallery, Next.js intercepts the navigation and renders @modal/(.)photo/[id]/page.tsx inside the modal slot — while the gallery stays visible in the background. If they paste the URL directly into a new tab, the interception doesn't apply, and they get photo/[id]/page.tsx as a standalone page. Same URL, two experiences. If you want to implement a theme toggle for your app's UI shell while building this, check out how we handle theme switching in React.

Building a URL-Driven Modal Step by Step

Let's put it together. This is a minimal but real implementation. The goal: a user list that opens a user profile modal when you click a name, updates the URL to /users/42, and closes back to the list when you press Escape or click outside.

// app/users/@modal/(.)users/[id]/page.tsx
import { UserModal } from '@/components/UserModal'

export default async function InterceptedUserPage({
  params,
}: {
  params: { id: string }
}) {
  const user = await fetchUser(params.id)

  return <UserModal user={user} />
}

// app/users/@modal/default.tsx
export default function Default() {
  return null
}

// app/users/layout.tsx
export default function UsersLayout({
  children,
  modal,
}: {
  children: React.ReactNode
  modal: React.ReactNode
}) {
  return (
    <>
      {children}
      {modal}
    </>
  )
}

The UserModal component itself is a regular client component. It reads useRouter() to navigate back on close. The modal wraps content in a fixed overlay, typically with a backdrop at something like rgba(0, 0, 0, 0.6) and a card container. The URL-driven aspect is entirely handled by Next.js routing — your component doesn't need to know anything special.

One thing to watch: make sure your modal close button calls router.back(), not router.push('/'). You want to undo the navigation, not push a new entry into the history stack. That's the difference between feeling native and feeling janky.

Handling Loading and Error States Per Slot

One of the underrated benefits of parallel routes is granular Suspense boundaries. Each slot can have its own loading.tsx and error.tsx. This means a slow database query in one slot doesn't freeze the entire page — just that slot shows a skeleton while everything else renders normally.

You've probably run into the pattern where a dashboard loads fine but one widget blocks the whole page. With parallel routes you can wrap each slot in its own Suspense bubble implicitly just by adding loading.tsx. For toast-style error feedback within a slot, React toast notifications pair well with the per-slot error boundaries to surface failures without nuking the whole layout.

// app/dashboard/@analytics/loading.tsx
export default function AnalyticsLoading() {
  return (
    <div className="animate-pulse rounded-xl bg-white/5 h-48 w-full" />
  )
}

// app/dashboard/@analytics/error.tsx
'use client'
export default function AnalyticsError({
  error,
  reset,
}: {
  error: Error
  reset: () => void
}) {
  return (
    <div className="p-4 rounded-xl border border-red-500/20 bg-red-500/10">
      <p className="text-sm text-red-400">{error.message}</p>
      <button onClick={reset} className="mt-2 text-xs text-red-300 underline">
        Try again
      </button>
    </div>
  )
}

Keep loading states lightweight. A 16px height skeleton bar or an animate-pulse div with rgba(255,255,255,0.05) background is enough visual feedback. Don't over-engineer it — the point is to not block the user, not to dazzle them with spinners.

Common Pitfalls and How to Avoid Them

The most frequent mistake is missing default.tsx files — covered above, but worth repeating because it'll bite you every time you add a new slot. The second most common issue is confusing the interception level syntax. Drawing your route tree on paper before picking (.), (..), or (...) saves real debugging time.

Navigation behavior can surprise you. When you use <Link> inside a slot, it only affects that slot's portion of the URL if you're careful about your href values. If you push a URL that doesn't match a slot's folder structure, you might accidentally navigate the entire page instead of just the slot. Test with the Network tab open to see exactly what requests Next.js fires.

Another gotcha: parallel routes don't work with the old Pages Router. They're App Router only, requiring Next.js 13.3 or later — and realistically you want 14.x or 15.x for stability. If your project still mixes Pages and App Router directories, the @slot convention only applies to the app/ side. Also worth noting: if you're doing anything performance-sensitive in these layouts, the React performance guide has concrete patterns for memoization and avoiding unnecessary re-renders in complex layout trees.

Finally, be deliberate about when you actually need parallel routes versus simpler state management. A modal that doesn't need a shareable URL? Just use useState. Parallel routes add file system complexity — use them when the URL-driven, server-rendered, independently-loading behavior is genuinely what you need.

Real-World Patterns Worth Stealing

A few patterns keep coming up when you use parallel routes in production. The split-view editor — think Notion or Linear — where left navigation and right content each have their own route history. The notification drawer that slides in over content without unmounting the background page. The multi-step checkout that tracks each step in the URL without full page transitions.

For SaaS dashboards especially, the combination of parallel routes for layout slots and intercepting routes for quick-view modals can replace a whole layer of client-side state management. Instead of Redux or Zustand holding 'which modal is open', the URL holds that state. It's shareable, it survives refresh, and back-button works for free. What about the visual design of those modals? Glassmorphism effects work particularly well for modal overlays — that frosted glass look at blur(12px) with rgba(255,255,255,0.08) background keeps the content behind visible while clearly separating the modal layer.

Is every team going to need parallel routes? Honestly, no. Simple apps with simple navigation don't need this complexity. But if you're building anything where the URL should reflect multiple pieces of active UI state simultaneously — a real-time feed with a detail panel, a code editor with a file tree and output console, a social feed with a photo viewer — parallel routes are the right tool. And once you understand how the @slot naming maps to layout props, the implementation is actually less code than the stateful alternative.

FAQ

Do parallel routes work with the Next.js Pages Router?

No. Parallel routes (@slot folders) and intercepting routes ((.) prefixes) are App Router-only features. They require Next.js 13.3 at minimum, and you'll want 14.x or 15.x for a stable experience. If you're on the Pages Router, you'll need to handle split-UI patterns with client-side state.

What happens if I forget to add default.tsx to a slot?

Next.js will throw a 404 error for the entire page when it navigates to a URL that doesn't match any page inside that slot's folder. It's not a subtle warning — the page hard-errors. Always add a default.tsx that returns null to every slot folder as soon as you create it.

How do I close a modal opened via an intercepting route?

Call router.back() from the useRouter() hook (client component). This undoes the navigation that opened the modal, returning the user to the previous route. Don't use router.push('/') unless you actually want to push a new history entry — that would break the expected back-button behavior.

Can two parallel slots navigate independently of each other?

Yes, that's the whole point. Each slot maintains its own navigation state. A <Link> in @sidebar updates the sidebar's segment without affecting @feed or children. This is what enables dashboard layouts where the side panel has its own drill-down navigation while the main content stays in place.

What's the difference between `(.)` and `(..)` in intercepting routes?

(.) intercepts a route at the same directory level as the intercepting folder. (..) intercepts one level up in the route hierarchy. (..)(..) goes two levels up, and (...) intercepts from the app root. Pick based on where the target route lives relative to your @slot folder — draw your folder tree first to get it right.

Can I use parallel routes with React Server Components and async data fetching?

Yes, and this is one of their strengths. Each slot's page.tsx can be an async Server Component that fetches its own data. Because they render in parallel (hence the name), Next.js fires those requests simultaneously rather than waterfalling them. Each slot also gets its own loading.tsx Suspense boundary so slow fetches don't block faster slots.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

React Server Actions: Complete Guide for Next.js App RouterNext.js Image Optimization: Every Setting and Its Trade-OffCore Web Vitals in 2026: LCP, INP, CLS with Real Next.js FixesScroll Progress Indicator: Reading Bar in Next.js App Router