Parallel Routes — @slot
Renders several routes side by side in one layout, each passed to it as a named prop.
app/@analytics/page.js -> passed to app/layout.js as the `analytics` prop
app/@team/page.js -> passed to app/layout.js as the `team` prop
export default function Layout({
children,
analytics,
team,
}: {
children: React.ReactNode
analytics: React.ReactNode
team: React.ReactNode
}): React.ReactNodeParameters
| Parameter | Type | Description |
|---|---|---|
$children | React.ReactNode | The implicit slot. app/page.js is equivalent to app/@children/page.js, so it needs no @ folder of its own. |
$@folderName | React.ReactNode | A named slot. The folder app/@analytics is passed to the sibling layout as a prop called analytics. The @ prefix is stripped and the slot never appears in the URL. |
$default.js | file | Fallback rendered for a slot that does not match the current URL on initial load or a full-page reload. Required for every slot, children included. |
Return value
React.ReactNodeEach slot resolves to a React node that the parent layout places wherever it wants; slots do not add segments to the URL.
Errors
A hard navigation to a URL a slot cannot match renders a 404 when that slot has no default.js.
Changelog
- PHP 16.0.0All parallel route slots now require an explicit default.js file; builds fail without them. Return null or call notFound() to keep the previous behaviour.
An @-prefixed folder is a slot: it becomes a prop on the sibling layout instead of a URL segment. app/@analytics/page.js arrives as analytics, and the layout decides where to put it. Each slot streams independently, so a slow panel gets its own loading.js and error.js without holding up the rest of the view.
When to reach for it
- Dashboards where panels load at different speeds and should not block one another.
- Showing an admin and a user variant of the same route from one layout.
- Modals that deep-link, paired with an intercepting route.
What trips people up
Navigation behaviour splits in two. On a client-side transition Next.js keeps each slot's active subpage even when it no longer matches the URL; on a refresh it cannot recover that state and falls back to default.js, or 404s without one. Since Next.js 16 the build fails outright if a slot has no default.js.
Conditional rendering is not authorization. {role === 'admin' ? admin : user} still executes @admin/page.js and its data fetches for every visitor, and the result ships in the response. Authorize inside each slot.
Example
export default function Layout({
children,
team,
analytics,
}: {
children: React.ReactNode
analytics: React.ReactNode
team: React.ReactNode
}) {
return (
<>
{children}
{team}
{analytics}
</>
)
}
// app/dashboard/
// ├── layout.tsx
// ├── page.tsx the implicit `children` slot
// ├── @team/
// │ ├── page.tsx
// │ └── default.tsx
// └── @analytics/
// ├── page.tsx
// └── default.tsxWhen to use it
- Stream a dashboard's analytics and team panels independently with separate loading states.
- Show an admin panel or a user panel from the same URL based on role.
- Open a photo in a modal over the feed while keeping a shareable /photo/123 URL.
- Give one section of a page its own tabs and its own back/forward history.
Frequently asked questions
Does an @folder change the URL in Next.js?
No. Slots are not route segments and do not affect the URL structure — app/@analytics/views resolves to /views. They are passed to the shared parent layout as props instead.
Why do my parallel routes 404 on refresh but work when I click?
Because a full-page load cannot recover each slot's active state, so Next.js renders default.js for the slots that do not match the URL and 404s when that file is missing. Add a default.js to every slot, including children.
Do I need default.js for every parallel route slot?
Yes, as of Next.js 16 — all slots require an explicit default.js and builds fail without them. Return null to render nothing, or call notFound() to keep the older 404 behaviour.
More examples
default.js for an unmatched slot
Consider @team having a /settings page while @analytics does not. Clicking through to /settings renders the new @team page and leaves @analytics showing whatever it had — but pressing reload loses that state, and Next.js renders this file instead. Without it the whole page 404s, which is why the bug always reproduces on refresh and never on click.
// app/dashboard/@analytics/default.tsx
export default function Default() {
return null
}
// Or preserve the pre-16 behaviour of 404-ing:
// import { notFound } from 'next/navigation'
//
// export default function Default() {
// notFound()
// }Conditional slots are not access control
Both slots render on the server regardless of which one the layout returns, and the discarded slot's output is included in the response sent to the browser. The ternary decides what the user sees, not what runs — so getAdminStats must do its own authorization, or in your Data Access Layer.
// app/dashboard/layout.tsx
import { checkUserRole } from '@/lib/auth'
export default function Layout({
user,
admin,
}: {
user: React.ReactNode
admin: React.ReactNode
}) {
const role = checkUserRole()
return role === 'admin' ? admin : user
}
// app/dashboard/@admin/page.tsx
import { getAdminStats } from '@/lib/dal'
export default async function AdminPage() {
// Runs for every visitor, admin or not. Authorize here.
const stats = await getAdminStats()
return <Stats stats={stats} />
}Closing a modal slot on navigation
router.back() closes the modal by unwinding history, which is why the browser back button also closes it. The catch-all is the non-obvious half: a client-side navigation to a route the slot does not match leaves the slot's last content on screen, so the modal would stay open when linking to /foo. Matching every other route to a component that returns null is what actually dismisses it.
// app/@auth/default.tsx — not rendered when inactive
export default function Default() {
return null
}
// app/@auth/[...catchAll]/page.tsx — closes the modal on any other route
export default function CatchAll() {
return null
}
// app/ui/modal.tsx
'use client'
import { useRouter } from 'next/navigation'
export function Modal({ children }: { children: React.ReactNode }) {
const router = useRouter()
return (
<>
<button onClick={() => router.back()}>Close modal</button>
<div>{children}</div>
</>
)
}
Discussion