Skip to content
Codeloom
Next.js

Next.js Parallel Routes with the @folder Convention

Learn how to render multiple pages simultaneously in the same layout using Next.js parallel routes and the @folder naming convention.

·6 min read · By Codeloom
Intermediate 12 min read

What you'll learn

  • What parallel routes are and when to use them
  • How the @folder convention creates named slots
  • Rendering multiple page segments inside a single layout
  • Handling unmatched routes with default.tsx
  • Conditional rendering based on authentication state

Prerequisites

  • Working knowledge of the [Next.js App Router](/blog/nextjs-app-router-basics)
  • Familiarity with [layouts and nested routing](/blog/nextjs-dynamic-routes-and-catchall)

Parallel routes let you render more than one page component inside the same layout at the same time. Instead of replacing the entire view when a user navigates, you can independently load and display separate sections of the UI. Think dashboards with a main panel and a sidebar that each have their own navigation, or a feed page that also renders a modal without unmounting the feed.

The @folder convention

Next.js uses folder names that start with @ to define named slots. Each slot acts as a separate route segment that gets passed to the parent layout as a prop.

app/
  layout.tsx
  page.tsx
  @analytics/
    page.tsx
  @feed/
    page.tsx

The parent layout.tsx receives every slot as a prop alongside children:

// app/layout.tsx
export default function DashboardLayout({
  children,
  analytics,
  feed,
}: {
  children: React.ReactNode;
  analytics: React.ReactNode;
  feed: React.ReactNode;
}) {
  return (
    <div className="grid grid-cols-3 gap-4">
      <main className="col-span-2">{children}</main>
      <aside>
        {analytics}
        {feed}
      </aside>
    </div>
  );
}

The children prop maps to the implicit slot for the page.tsx that sits directly inside app/. The analytics and feed props correspond to the @analytics and @feed folders.

Independent loading states

Because each slot is a separate route segment, you can add a loading.tsx file inside any @ folder to show a per-slot skeleton:

app/
  @analytics/
    loading.tsx
    page.tsx
  @feed/
    loading.tsx
    page.tsx
// app/@analytics/loading.tsx
export default function AnalyticsLoading() {
  return <div className="animate-pulse h-48 bg-gray-200 rounded" />;
}

When the page first loads, each slot streams in independently. If the analytics query takes three seconds but the feed resolves in 200 milliseconds, the user sees the feed immediately while the analytics panel shows a skeleton. This is one of the biggest advantages over a single-page approach where the slowest data fetch blocks the entire render.

Handling unmatched routes with default.tsx

When a user navigates to a route that exists in one slot but not another, Next.js needs to know what to render for the missing slot. Without explicit handling, you get a 404. The fix is a default.tsx file:

// app/@analytics/default.tsx
export default function AnalyticsDefault() {
  return <p>Select a date range to view analytics.</p>;
}

The default.tsx acts as a fallback. It renders when the current URL does not match any page inside that slot. You will almost always want a default.tsx in every parallel route folder to avoid unexpected 404 pages during soft navigation.

Nested parallel routes

Slots can have their own nested routes. This lets each slot respond to different URL segments independently:

app/
  layout.tsx
  page.tsx
  @team/
    page.tsx
    [memberId]/
      page.tsx
  @activity/
    page.tsx
    [memberId]/
      page.tsx
    default.tsx

Navigating to /team/42 renders the @team/[memberId]/page.tsx slot and the @activity/[memberId]/page.tsx slot in parallel, each receiving { params: { memberId: '42' } }.

Conditional slots based on auth

A powerful pattern is choosing which slot to render based on the user’s authentication state. Because the layout receives slots as props, you can conditionally display them:

// app/layout.tsx
import { getSession } from '@/lib/auth';

export default async function RootLayout({
  children,
  dashboard,
  login,
}: {
  children: React.ReactNode;
  dashboard: React.ReactNode;
  login: React.ReactNode;
}) {
  const session = await getSession();

  return (
    <html lang="en">
      <body>
        {session ? dashboard : login}
        {children}
      </body>
    </html>
  );
}
app/
  layout.tsx
  page.tsx
  @dashboard/
    page.tsx
    default.tsx
  @login/
    page.tsx
    default.tsx

This avoids redirects. Both the dashboard and login components are rendered on the server, but only the relevant one is sent to the client based on the session check.

Combining parallel routes with intercepting routes

Parallel routes pair naturally with intercepting routes to build modal patterns. You can use a parallel slot as the modal container and an intercepting route to capture navigation without a full page transition:

app/
  layout.tsx
  @modal/
    (.)photo/[id]/
      page.tsx
    default.tsx
  feed/
    page.tsx
  photo/[id]/
    page.tsx

The layout renders the @modal slot on top of children. When a user clicks a photo link from the feed, the intercepting route (.)photo/[id] renders inside the modal slot while the feed stays visible underneath. A hard refresh or direct URL visit skips the intercept and renders the full photo/[id]/page.tsx instead.

Error handling per slot

Each slot can have its own error.tsx boundary:

// app/@analytics/error.tsx
'use client';

export default function AnalyticsError({
  error,
  reset,
}: {
  error: Error;
  reset: () => void;
}) {
  return (
    <div className="p-4 bg-red-50 rounded">
      <p>Failed to load analytics: {error.message}</p>
      <button onClick={reset} className="mt-2 text-blue-600 underline">
        Retry
      </button>
    </div>
  );
}

If the analytics slot throws, only that slot shows the error UI. The feed and the main content remain fully interactive.

Common pitfalls

Forgetting default.tsx is the most frequent issue. Without it, client-side navigation to a route that does not match a slot will render a 404 for that slot, which can break the entire layout.

Slots are not URL segments. The @analytics folder does not add /analytics to the URL. It is purely an organizational convention that tells the router to inject the rendered component as a prop.

Hard vs. soft navigation matters. On a soft navigation (clicking a <Link>), Next.js preserves the previously active state of unmatched slots. On a hard navigation (full page load), unmatched slots render their default.tsx. Design your defaults accordingly.

When to use parallel routes

Parallel routes shine in these scenarios:

  • Dashboards where multiple panels update independently
  • Split views like an email client with a list and a detail pane
  • Modals that overlay content without unmounting the underlying page
  • Conditional layouts that swap sections based on user role or feature flags

For simpler pages where only one main content area changes at a time, standard nested routes are sufficient. Parallel routes add complexity, so reach for them when you genuinely need independent, simultaneously rendered page segments.