Some interfaces do not fit the "one URL, one page" model: a dashboard shows several independent panels, and a photo gallery opens images in a modal that still has its own shareable URL. Parallel routes and intercepting routes are the App Router's answer. After this lesson you will be able to render multiple route slots inside one layout, give each slot its own loading and error state, and open a route as a modal while keeping full-page rendering for direct visits.
A folder prefixed with @ is a slot. Slots are not URL segments; they are passed to the parent layout as props, alongside the implicit children slot:
app/dashboard/
├── layout.tsx
├── page.tsx # children slot
├── @analytics/
│ ├── page.tsx
│ └── loading.tsx # independent loading state
└── @team/
└── page.tsx// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
analytics,
team,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
team: React.ReactNode;
}) {
return (
<section className="grid">
{children}
<aside>{analytics}</aside>
<aside>{team}</aside>
</section>
);
}Each slot streams independently and can have its own loading.tsx and error.tsx, so a failing analytics query does not take down the team panel. Slots can also be rendered conditionally, for example showing an @admin slot only when the current user has the right role.
Slots keep their previous content during client-side navigation, but on a hard reload Next.js has to render every slot for the current URL. If a slot has no page matching that URL, it looks for default.tsx in the slot folder and renders a 404 if none exists. Add one to every slot:
// app/dashboard/@team/default.tsx
export default function Default() {
return null;
}Inside a Client Component, useSelectedLayoutSegment("analytics") returns the active segment of a specific slot, which is handy for tabs that highlight the current panel.
An intercepting route renders another route's content inside the current layout during client-side navigation, while a direct visit or refresh still renders the original page. The folder name says which route to intercept, relative to the current segment:
| Prefix | Intercepts |
|---|---|
| (.) | Same level |
| (..) | One level up |
| (..)(..) | Two levels up |
| (...) | From the app root |
Levels refer to route segments, not folders; slots such as @modal do not count.
Combine both features, as shown in the sample at the top of this lesson. Clicking a grid item navigates to /photos/123; the @modal slot intercepts it and renders the photo over the grid. The URL is shareable, and someone opening it directly gets the full photos/[id]/page.tsx instead.
// app/@modal/(.)photos/[id]/page.tsx
import { Modal } from "@/components/Modal";
import { getPhoto } from "@/lib/photos";
export default async function PhotoModal({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const photo = await getPhoto(id);
return (
<Modal>
<img src={photo.url} alt={photo.alt} />
</Modal>
);
}The Modal component is a Client Component that closes with router.back(), which restores the grid and clears the slot. Remember the default.tsx returning null in @modal, otherwise every non-photo URL fails to render the slot.
default.tsx and getting a 404 on refresh for pages that worked during navigation.@slot folders when choosing (.) versus (..); only real segments count.router.push("/") instead of router.back(), which leaves the interception active.How does a layout receive the content of `app/dashboard/@team/page.tsx`?
@slot folders create parallel routes passed to the layout as props, each with its own loading and error states.default.tsx so hard navigations can render unmatched slots.(.), (..), (...) show another route inside the current layout during client-side navigation.router.back().Next lesson: Server Actions and Forms — mutate data on the server directly from a form without writing an API route.