cf.completefrontend
Open lab My progress
THE NEXT.JS FIELD GUIDE

Parallel routes

Render several pages in one layout with @slot folders: a staff dashboard whose panels navigate, load and fail on their own, with default.tsx for reloads.

By the end, you can
  • 01
    Build a layout from slotsAdd @slot folders, place each slot in a layout, and say which address every page in a slot has.
  • 02
    Know what each slot showsPredict what a slot shows after a click and after a reload, and write the default.tsx Next.js 16.4 asks for.
  • 03
    Give each slot its own states and checksAdd loading.tsx and error.tsx to one slot, and check a visitor's role inside the slot that needs it.

Two panels beside the page, from two folders

The tea shop's staff start each day on a dashboard. It shows the day's orders and the stock side by side. You could draw both in one page component. But each panel has its own data, and soon its own loading message, its own error screen and its own tabs. Parallel routes let one layout show several pages at the same time, each from a folder of its own.

A folder whose name starts with @, like @orders, is a slot. The page.tsx inside it is a page like any other, but it doesn't get an address of its own. Instead, it goes to the layout of the folder above, as a prop named after the slot: @orders becomes the prop orders. Templates and the file hierarchy gave a first look at a single slot. Here is the dashboard with two, as a real Next.js 16.4.3 project: built with next build, run with next start, and recorded.

The staff dashboard: a page and two slotsNext.js
app/dashboard/@orders/@stock/
app/dashboard/layout.tsx
export default function DashboardLayout({ children, orders, stock }: LayoutProps<"/dashboard">) {  return (    <main>      {children}      <section aria-label="Orders">{orders}</section>      <section aria-label="Stock">{stock}</section>    </main>  );}
Recorded from Next.js 16.4.3
localhost:3000/dashboardTea shop staff
Home
Today at the tea shop
Orders
  • Order 1041: Green tea
  • Order 1042: Mint tea
Stock

Green tea: 12 boxes

Mint tea: 3 boxes

The page as the browser shows it when its HTML arrives, drawn with plain styles. Its JavaScript doesn't run here.

The file shown first is app/dashboard/layout.tsx. Line 1 takes three props: children, orders and stock. Lines 4 to 6 place them: the page first, then the orders and the stock, each in a <section> with a label. LayoutProps<"/dashboard"> is the type Next.js writes for this layout. It knows the folder holds @orders and @stock, so it includes both props, and the build's type check passed.

Open the Terminal tab. The route table lists ○ /dashboard, and no address with orders or stock in it. In the Browser tab, /dashboard shows Today at the tea shop, then the orders and the stock. Pick /dashboard/orders and /dashboard/@orders: both answered 404. A slot is not a route segment (one part of the address, between two slashes), so its name never shows in the address, with or without the @.

There's a third slot in this layout: children. It has no @ folder. The page beside the layout, app/dashboard/page.tsx, fills it, the way a page always fills its layout's children. Next.js treats it like the other slots, and its error messages name it beside them when a slot has nothing to show. Each row is one page file of the dashboard: the prop it fills, and what it shows at /dashboard.

The dashboard's page files
FileThe prop it fillsWhat it shows
app/dashboard/page.tsxchildrenToday at the tea shop
app/dashboard/@orders/page.tsxordersOrders, and two orders
app/dashboard/@stock/page.tsxstockStock, and two teas
An editor window for the dashboard folder lists page.tsx, @orders/page.tsx and @stock/page.tsx. An arrow goes from each file to one part of a browser window at slash dashboard: children, orders and stock.dashboardpage.tsx@orders/page.tsx@stock/page.tsx/dashboardchildrenordersstockthe layout places each one; no @ folder is part of the address
One address, three parts. page.tsx fills children, and each @ folder fills the prop with its name. All three show at /dashboard.
Real-life analogyA lunch box with compartments

A lunch box has one name on its lid, but inside it has a big compartment and a few small ones. The cook fills each one from a different pot. Tomorrow, the cook can swap what's in one small compartment and leave the others as they are.

In real life: The lunch box, with one name on the lid
In Next.js: The layout, at one address: /dashboard
In real life: The big compartment, for the main dish
In Next.js: children, filled by page.tsx
In real life: The small compartments, each with a label
In Next.js: The slots, @orders and @stock
In real life: Each compartment filled from its own pot
In Next.js: Each slot's page, from its own folder
In real life: Swapping the food in one compartment, and leaving the rest
In Next.js: A click that changes one slot and keeps the others
The main idea

A folder named @something is a slot: its pages render inside the parent layout, as the prop something, at the same address as the layout's page. The layout places the slots where it wants them. children is a slot too, filled by the page.tsx beside the layout.

Clicking inside one slot

A slot can have more than one page. The orders slot gets a second tab, for late orders: a page at app/dashboard/@orders/late/page.tsx. @orders adds nothing to the address and late adds one segment, so it's the page for /dashboard/late. A layout.tsx inside the slot holds the tabs, so both of the slot's pages share them, the way Layouts and pages showed for ordinary folders.

Tabs inside the orders slotNext.js
app/dashboard/@orders/late/@stock/
app/dashboard/@orders/layout.tsx
import OrderTabs from "./order-tabs"; export default function OrdersLayout({ children }: { children: React.ReactNode }) {  return (    <>      <h2>Orders</h2>      <OrderTabs />      {children}    </>  );}
Recorded from Next.js 16.4.3

Each step was taken in real Chrome against next start. Pick one to see the page after it.

localhost:3000/dashboard/lateTea shop staff
Home Dashboard
Today at the tea shop
Orders
All orders Late orders
  • Order 1042: Mint tea, 2 days late
Stock

Green tea: 12 boxes

Mint tea: 3 boxes

Focus
the link “Late orders”
Scrolled
0 px down

No requests during this step: the page already had what it needed

In app/dashboard/@orders/layout.tsx, the file shown first, line 6 is the slot's heading, line 7 draws the tabs, and line 8 is the slot's page. The tabs are in app/dashboard/@orders/order-tabs.tsx, a Client Component. Its line 7 calls useSelectedLayoutSegment(), a hook from next/navigation that returns the segment that's active one level below the layout it's called in: null for the slot's own page.tsx, and "late" for late/page.tsx. Lines 10 and 13 use it to put aria-current="page" on the open tab, which tells screen readers which one it is. The recorded page draws that link in bold. A layout higher up can ask about one slot by its name, as in useSelectedLayoutSegment("orders").

The Steps tab lists five steps, taken in real Chrome. It opens on step 3:

  • Step 1, open /dashboard: one request for the whole page, then prefetches: the router asking early for the pages the links point to, before anyone clicks. Linking and navigating showed them.
  • Step 2 waits until the four prefetches for /dashboard/late have answered.
  • Step 3, click Late orders: the address is /dashboard/late, and the orders slot shows Order 1042: Mint tea, 2 days late. Today at the tea shop and the stock are still on the screen. The step made no request at all: the prefetches had brought what the click needed.

Look at what step 3 kept. /dashboard/late has a page only in @orders. Neither children nor @stock has a late folder. On a client-side navigation, where the router swaps parts of the page without loading a new HTML page, Next.js keeps showing each slot's last page when the new address has nothing for that slot. Next.js's docs call this a soft navigation. Each slot remembers its own active page, so one slot can move between tabs while the others stay put.

After a reload: default.tsx

Step 4 reloads /dashboard/late. A reload, an address typed into the browser, or a link from another site is a hard navigation: a full page load, where the browser asks the server for a whole new HTML page. The server only has the address. It can't know which page the stock slot showed a moment ago, so there's nothing to keep.

That's what default.tsx is for. It's what a slot shows when it has no page for the address and no earlier page to keep. This dashboard has two:

  • app/dashboard/@stock/default.tsx returns Stock is on the dashboard's first tab., for the stock slot.
  • app/dashboard/default.tsx returns Dashboard, for children.
The same tabs, after a reloadNext.js
app/dashboard/@orders/late/@stock/
app/dashboard/@stock/default.tsx
export default function StockDefault() {  return <p>Stock is on the dashboard's first tab.</p>;}
Recorded from Next.js 16.4.3

Each step was taken in real Chrome against next start. Pick one to see the page after it.

localhost:3000/dashboard/lateTea shop staff
Home Dashboard
Dashboard
Orders
All orders Late orders
  • Order 1042: Mint tea, 2 days late

Stock is on the dashboard's first tab.

Focus
nothing: the page itself
Scrolled
0 px down

Requests the page made during this step (8), without the files it loaded

  1. GET/dashboard/late200a whole page
  2. GET/200text/x-component
    rsc
    1
    next-router-prefetch
    1
    next-router-segment-prefetch
    /_tree
    next-url
    /dashboard/late
  3. GET/dashboard200text/x-component
    rsc
    1
    next-router-prefetch
    1
    next-router-segment-prefetch
    /_tree
    next-url
    /dashboard/late
  4. GET/dashboard/late200text/x-component
    rsc
    1
    next-router-prefetch
    1
    next-router-segment-prefetch
    /_tree
    next-url
    /dashboard/late
  5. GET/200text/x-component
    rsc
    1
    next-router-prefetch
    1
    next-router-segment-prefetch
    /__PAGE__
    next-url
    /dashboard/late
  6. GET/dashboard200text/x-component
    rsc
    1
    next-router-prefetch
    1
    next-router-segment-prefetch
    /dashboard/__PAGE__
    next-url
    /dashboard/late
  7. GET/dashboard200text/x-component
    rsc
    1
    next-router-prefetch
    1
    next-router-segment-prefetch
    /dashboard/@orders/!KF9fU0xPVF9fKQ/__PAGE__
    next-url
    /dashboard/late
  8. GET/dashboard200text/x-component
    rsc
    1
    next-router-prefetch
    1
    next-router-segment-prefetch
    /dashboard/@stock/!KF9fU0xPVF9fKQ/__PAGE__
    next-url
    /dashboard/late

In step 4, the page shows Dashboard from the first default, the late orders from @orders/late/page.tsx, and Stock is on the dashboard's first tab. from the stock's default. Its requests start with one document, the whole page. Step 5 clicks All orders, and all three slots have pages for /dashboard again. Each row is one slot at /dashboard/late: what it showed after the click, and after the reload.

One address, two ways in
SlotAfter a click from /dashboardAfter a reload
childrenToday at the tea shop, kept from /dashboard.Dashboard, from app/dashboard/default.tsx.
ordersThe late orders, from @orders/late/page.tsx.The late orders, from @orders/late/page.tsx.
stockThe stock, kept from /dashboard.Stock is on the dashboard's first tab., from @stock/default.tsx.
Two browser windows, both at slash dashboard slash late. The left one, after a click, shows children: kept page, orders: late/page.tsx, and stock: kept page. The right one, after a reload, shows children: default.tsx, orders: late/page.tsx, and stock: default.tsx.after a clickchildren: kept pageorders: late/page.tsxstock: kept pageafter a reloadchildren: default.tsxorders: late/page.tsxstock: default.tsxa client-side navigationa full page load
The same address, two ways in. After a click, Next.js keeps the slots that have no page for /dashboard/late. After a reload, it only has the address, so those slots show their default.tsx.

Now leave files out. Each version below changes only the default.tsx files. Before you click, guess which ones build, and what a reload of /dashboard/late shows in each.

Playground: what do the slots show on a reload of /dashboard/late?
The staff dashboard, with these default.tsx filesNext.js
app/dashboard/@orders/late/@stock/
app/dashboard/layout.tsx
export default function DashboardLayout({ children, orders, stock }: LayoutProps<"/dashboard">) {  return (    <main>      {children}      <section aria-label="Orders">{orders}</section>      <section aria-label="Stock">{stock}</section>    </main>  );}
next build failed · Next.js 16.4.3
$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled   Creating an optimized production build ...⚠ Strict route matching is enabled by default. This validation indicates a bug in your app's route structure, but you can temporarily restore loose route matching by setting `deprecated.looseRouteMatching` to `true` in your Next.js config. > Build error occurredError: Turbopack build failed with 2 errors:./app/dashboard/layout.tsxError: Parallel route slots cannot render the same URLsThe following layouts have parallel route slots that cannot render the same URLs:app/dashboard/layout.tsx- /dashboard/late is missing a matching page or default.tsx in @stock, children Every URL matched by one slot must have a matching page or default.tsx in every sibling slot. ./app/dashboard/@orders/late/page.tsxError: Unmatched app pagesThe following page files do not match any complete route:- app/dashboard/@orders/late/page.tsx Every page must be part of at least one complete route. Add matching pages or default files for the sibling parallel route slots, or remove the unreachable pages.     at ignore-listed frames
Try it yourself
No default.tsx

Neither @stock nor children has anything for /dashboard/late. The build stops: /dashboard/late is missing a matching page or default.tsx in @stock, children.

Every version was built with next build from Next.js 16.4.3. The ones that built were clicked through in real Chrome: open /dashboard, wait for the prefetches, click Late orders, then reload /dashboard/late. A build that stopped shows its Terminal instead. Reset goes back to the first version.

Without any default.tsx, the build stops: /dashboard/late is missing a matching page or default.tsx in @stock, children, and Every URL matched by one slot must have a matching page or default.tsx in every sibling slot. A second message, Unmatched app pages, lists app/dashboard/@orders/late/page.tsx: with nothing beside it in the other slots, that page could never be shown. With only the stock's default, the build names children alone. With defaults that call notFound(), the click still keeps every slot, and the reload answers 404 for the whole page, with This page could not be found.

The docs and the recordings say this in two different ways:

  • The docs. The Next.js 16 upgrade guide says all parallel route slots now need a default.js, and that builds fail without them. It suggests one that calls notFound() to keep the older behavior: a 404 on the reload. The docs page for default.js still says a missing default for children gives a 404.
  • What Next.js 16.4.3 did here. Its strict route matching, on by default since Next.js 16.4, checks each address the layout can show against every slot, children included. A slot needs a default.tsx only when one of those addresses has no page in it: the first dashboard had one address and a page in every slot, and it built without any default.tsx. When children had none for /dashboard/late, the build stopped instead of answering 404. Its warning is the line that starts with ⚠ at the top of each failed build.

The last version is the one most dashboards want. Each default is one line, which shows the slot's usual page:

app/dashboard/@stock/default.tsx, showing the stock pageTSX
export { default } from "./page";

After a reload of /dashboard/late, the heading and the stock look exactly as they did after the click. So choose, for each slot, what it should show when nothing matches: its usual page, nothing (return null), a message, or a 404.

Read it in Next.js's own code

The switch is in lines 460 to 468 of server/config.ts: strict route matching is on by default, and deprecated.looseRouteMatching in next.config.ts turns it off. The warning calls that a temporary way back, and says the check found a bug in the app's routes.

Each slot loads and fails on its own

Each slot can have its own loading.tsx and error.tsx, the files from Loading UI and Error handling. In a slot's folder, they wrap that slot's pages. Here both panels ask the shop's services for their data. The services are a stand-in that the recorder runs beside the app, at the address in STAFF_API_URL: the order service answers with two orders, and the stock service answers 500, as if it were down.

A slot that loads, and a slot that failsNext.js
app/dashboard/@orders/@stock/
app/dashboard/@stock/error.tsx
"use client"; export default function StockError({ retry }: { retry: () => void }) {  return (    <>      <h2>Stock</h2>      <p>The stock did not load.</p>      <button onClick={retry}>Try again</button>    </>  );}
Recorded from Next.js 16.4.3

GET /dashboard200

Response headers
Vary rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch, Accept-Encoding
x-nextjs-stale-time 300
x-nextjs-prerender 1
x-nextjs-postponed 1
X-Powered-By Next.js
Cache-Control private, no-cache, no-store, max-age=0, must-revalidate
Content-Type text/html; charset=utf-8

The HTML that arrived, one element per line, with the code of its 17 scripts left out

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="preload" as="script" fetchpriority="low" href="/_next/static/chunks/1x6obpmlu1_c2.js">
    <script src="/_next/static/chunks/06bs-nf19b2js.js" async=""></script>
    <script src="/_next/static/chunks/2ki52vdnbo2ft.js" async=""></script>
    <script src="/_next/static/chunks/3l_qmp-61z4yc.js" async=""></script>
    <script src="/_next/static/chunks/turbopack-3-ry682tkyiqk.js" async=""></script>
    <script src="/_next/static/chunks/3oh00mkbkq8u6.js" async=""></script>
    <script src="/_next/static/chunks/1z8gtk4_z506a.js" async=""></script>
    <title>Tea shop staff</title>
    <script src="/_next/static/chunks/0cz1d0mv5g_q7.js" nomodule=""></script>
  </head>
  <body>
    <div hidden="">
      <!--$-->
      <!--/$-->
    </div>
    <nav><a href="/">Home</a> <a href="/dashboard">Dashboard</a></nav>
    <!--&-->
    <main>
      <!--&-->
      <h1>Today at the tea shop</h1>
      <!--$-->
      <!--/$-->
      <!--/&-->
      <section aria-label="Orders">
        <!--&-->
        <!--&-->
        <!--$?-->
        <template id="B:0"></template>
        <p>Loading orders…</p>
        <!--/$-->
        <!--/&-->
        <!--/&-->
      </section>
      <section aria-label="Stock">
        <!--&-->
        <!--&-->
        <!--$?-->
        <template id="B:1"></template>
        <p>Loading stock…</p>
        <!--/$-->
        <!--/&-->
        <!--/&-->
      </section>
    </main>
    <!--/&-->
    <script>…</script>
    <script id="_R_">…</script>
    <script src="/_next/static/chunks/1x6obpmlu1_c2.js" async=""></script>
    <script>…</script>
    <script>…</script>
    <script>…</script>
    <script src="/_next/static/chunks/0cz1d0mv5g_q7.js" nomodule=""></script>
    <script>…</script>
    <div hidden="" id="S:0">
      <h2>Orders</h2>
      <ul>
        <li>Order <!-- -->1041<!-- -->: <!-- -->Green tea</li>
        <li>Order <!-- -->1042<!-- -->: <!-- -->Mint tea</li>
      </ul>
    </div>
    <script>…</script>
    <script>…</script>
  </body>
</html>

In app/dashboard/@stock/page.tsx, the file shown first, line 4 asks for the stock and line 5 throws when the answer isn't OK. In app/dashboard/@orders/page.tsx, line 4 asks for the orders. Each slot has a loading.tsx, and the stock slot also has an error.tsx, a Client Component whose line 8 offers Try again.

The Terminal tab says ◐ /dashboard: prerendered as static HTML with dynamic server-streamed content. With Cache Components on, as in every example here and in new projects, the build made a static shell for the page: everything that doesn't wait for a request. Both fetches wait for a service when a request comes in, so each slot's loading.tsx went into the shell in their place. The Network tab shows the HTML as it arrived. Its first part shows Today at the tea shop, Loading orders… and Loading stock…. Then the orders streamed into their slot. Loading stock… stayed in the HTML: error.tsx is an error boundary that React shows in the browser.

The same dashboard in ChromeNext.js
app/dashboard/@orders/@stock/
app/dashboard/@stock/error.tsx
"use client"; export default function StockError({ retry }: { retry: () => void }) {  return (    <>      <h2>Stock</h2>      <p>The stock did not load.</p>      <button onClick={retry}>Try again</button>    </>  );}
Recorded from Next.js 16.4.3

Each step was taken in real Chrome against next start. Pick one to see the page after it.

localhost:3000/dashboardTea shop staff
Home Dashboard
Today at the tea shop
Orders
  • Order 1041: Green tea
  • Order 1042: Mint tea
Stock

The stock did not load.

Try again
Focus
nothing: the page itself
Scrolled
0 px down

Requests the page made during this step (1), without the files it loaded

  1. GET/200text/x-component
    rsc
    1
    next-router-prefetch
    1
    next-router-segment-prefetch
    /__PAGE__
    next-url
    /dashboard

In Chrome, step 1 opened /dashboard and waited for The stock did not load., from line 7 of error.tsx. The step lists the error React logged in the console when the stock slot failed. Step 2 waited for the orders. After it, the page shows the heading, both orders, and the stock slot's error screen with its Try again button. One slot failed, and the other two kept working.

Two browser windows. The left one, the shell from the build, shows Today at the tea shop, Loading orders and Loading stock. Arrows go from the two loading cards to the right window, in Chrome, which shows the orders, and the stock slot's error screen.shellToday at the tea shopLoading orders…Loading stock…in ChromeToday at the tea shopOrder 1041, Order 1042The stock did not load.sent at onceeach slot on its own
The build made one shell for /dashboard, with each slot's loading fallback in it. Then each slot finished on its own: the orders arrived, and the stock slot showed its error.tsx.

A slot doesn't make its own route: the page and all its slots are one route, with one symbol in the route table and one shell. That's why the docs say you can't prerender one slot while another at the same level renders for each request. If one is dynamic, the route is. With Cache Components, that one route is ◐: its shell holds the static parts of every slot, and the parts that wait for a request stream in, each behind its own boundary. The first dashboard, which read nothing when a request came in, was ○ /dashboard, all of it prerendered.

Choosing a slot by role, on the server

A layout can also choose which slots to show. The shop's owner should see the day's sales, and the rest of the staff their next task. So the dashboard gets two slots, @admin and @staff, and its layout returns one of them by the visitor's role. Here the role is a cookie called role. That's a stand-in: anyone can set a cookie in their own browser, so a real app reads the role from a signed-in session, which the authentication lessons build.

A slot for the owner, a slot for staffNext.js
app/dashboard/@admin/@staff/
app/dashboard/layout.tsx
import { Suspense } from "react";import { cookies } from "next/headers"; async function ForRole({ admin, staff }: { admin: React.ReactNode; staff: React.ReactNode }) {  const role = (await cookies()).get("role")?.value;  return role === "admin" ? admin : staff;} export default function DashboardLayout({ children, admin, staff }: LayoutProps<"/dashboard">) {  return (    <main>      {children}      <Suspense fallback={<p>Checking your role…</p>}>        <ForRole admin={admin} staff={staff} />      </Suspense>    </main>  );}
Recorded from Next.js 16.4.3
localhost:3000/dashboardTea shop staff
Home
Today at the tea shop

Sales today: $1,240

Sent with cookie: role=admin. The page as the browser shows it when its HTML arrives, drawn with plain styles. Its JavaScript doesn't run here.

Lines 4 to 7 of app/dashboard/layout.tsx are ForRole, an async Server Component. Line 5 reads the cookie with cookies() from next/headers, and line 6 returns admin or staff. A cookie only exists when a request comes in, so lines 13 to 15 wrap ForRole in Suspense, with Checking your role… in the shell. Request-time APIs explains why. In the Browser tab, the owner, asked for with role=admin, sees Sales today: $1,240, and a member of staff sees Your next task: pack order 1042.

It looks right. Now open the Network tab and pick the third response: the RSC payload that the same member of staff gets. That's the data React uses to draw what the Server Components rendered; the router asks for it on client-side navigations. Search its body for Sales today: it's there. Next.js renders every slot of a route and sends each one's result, whatever the layout returns. The layout's condition decides what shows on the screen. It doesn't decide what runs, or what's sent.

So check the role where the data is: in the slot itself. Try both versions, and search each payload for the sales:

Playground: where should the role be checked?
The dashboard with a slot for each roleNext.js
app/dashboard/@admin/@staff/
app/dashboard/layout.tsx
import { Suspense } from "react";import { cookies } from "next/headers"; async function ForRole({ admin, staff }: { admin: React.ReactNode; staff: React.ReactNode }) {  const role = (await cookies()).get("role")?.value;  return role === "admin" ? admin : staff;} export default function DashboardLayout({ children, admin, staff }: LayoutProps<"/dashboard">) {  return (    <main>      {children}      <Suspense fallback={<p>Checking your role…</p>}>        <ForRole admin={admin} staff={staff} />      </Suspense>    </main>  );}
Recorded from Next.js 16.4.3

GET /dashboard?_rsc=200

Sent with the request
rsc 1
cookie role=staff
Response headers
Vary rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch, Accept-Encoding
Cache-Control private, no-cache, no-store, max-age=0, must-revalidate
Content-Type text/x-component

The body that arrived, as it arrived: the RSC payload, one row per line

2:"$Sreact.fragment"
3:"/_next/static/chunks/3oh00mkbkq8u6.js"
4:I[22016,["$3"],""]
5:I[39756,["$3"],"default"]
6:I[37457,["$3"],"default"]
9:"$Sreact.suspense"
d:I[97367,["$3"],"OutletBoundary"]
15:"ViewportBoundary"
16:I[97367,["$3"],"$15"]
18:"MetadataBoundary"
19:I[97367,["$3"],"$18"]
1:X
7:X
b:X
f:X
10:X
12:X
11:[["children",{"s":"__PAGE__","h":34,"d":{"r":["$","$2","c",{"children":[["$","p",null,{"children":"Sales today: $1,240"}],null,null]}],"p":false,"v":"$12"}}]]
14:X
13:[["children",{"s":"__PAGE__","h":34,"d":{"r":["$","$2","c",{"children":[["$","p",null,{"children":"Your next task: pack order 1042."}],null,null]}],"p":false,"v":"$14"}}]]
c:[["children",{"s":"__PAGE__","h":162,"d":{"r":["$","$2","c",{"children":[["$","h1",null,{"children":"Today at the tea shop"}],null,["$","$Ld",null,{"children":["$","$9",null,{"name":"Next.MetadataOutlet","children":"$@e"}]}]]}],"p":false,"v":"$f"}}],["admin",{"s":"(__SLOT__)","h":66,"d":{"r":["$","$2","c",{"children":[null,["$","$L5",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L6",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","forbidden":"$undefined","unauthorized":"$undefined"}]]}],"p":false,"v":"$10"},"c":"$Q11"}],["staff",{"s":"(__SLOT__)","h":66,"d":{"r":["$","$2","c",{"children":[null,["$","$L5",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L6",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","forbidden":"$undefined","unauthorized":"$undefined"}]]}],"p":false,"v":"$10"},"c":"$Q13"}]]
8:[["children",{"s":"dashboard","h":98,"d":{"r":["$","$2","c",{"children":[null,["$","main",null,{"children":[["$","$L5",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L6",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","forbidden":"$undefined","unauthorized":"$undefined"}],["$","$9",null,{"fallback":["$","p",null,{"children":"Checking your role…"}],"children":"$La"}]]}]]}],"p":false,"v":"$b"},"c":"$Qc"}]]
1b:X
1c:X
1f:R
0:{"q":"","i":false,"S":true,"r":"$1","b":"cf5eAV0mV1PbLin4mGz4e","t":{"t":{"s":"","h":82,"d":{"r":["$","$2","c",{"children":[[["$","script","script-0",{"src":"/_next/static/chunks/3oh00mkbkq8u6.js","async":true,"nonce":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":[["$","nav",null,{"children":[["$","$L4",null,{"href":"/","children":"Home"}]," ",["$","$L4",null,{"href":"/dashboard","children":"Dashboard"}]]}],["$","$L5",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L6",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]]}]}]]}],"p":false,"v":"$7"},"c":"$Q8"},"h":{"r":["$","$2","h",{"children":[null,["$","$L16","B2_SjCFpbRWVcISSk4J8mv",{"children":"$L17"}],["$","$L19","B2_SjCFpbRWVcISSk4J8mm",{"children":[["$","div",null,{"hidden":true,"children":["$","$9",null,{"name":"Next.Metadata","children":"$L1a"}]}],null]}]]}],"p":false,"v":"$1b"}},"s":"$1c","l":"$@1d","a":"$@1e","p":"$1f"}
10:C
1c:300
17:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]
e:null
1a:[["$","title","0",{"children":"Tea shop staff"}]]
1c:C
1b:C
1:C
f:C
12:C
14:C
b:C
7:C
a:["$","$L5",null,{"parallelRouterKey":"staff","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L6",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","forbidden":"$undefined","unauthorized":"$undefined"}]
1d:4702
1e:4666
1f:o1,#1f:o8e5,2:"$Sreact.fragment"
3:"/_next/static/chunks/3oh00mkbkq8u6.js"
4:I[22016,["$3"],""]
5:I[39756,["$3"],"default"]
6:I[37457,["$3"],"default"]
9:"$Sreact.suspense"
d:I[97367,["$3"],"OutletBoundary"]
15:"ViewportBoundary"
16:I[97367,["$3"],"$15"]
18:"MetadataBoundary"
19:I[97367,["$3"],"$18"]
1:X
7:X
b:X
f:X
10:X
12:X
11:[["children",{"s":"__PAGE__","h":34,"d":{"r":["$","$2","c",{"children":[["$","p",null,{"children":"Sales today: $1,240"}],null,null]}],"p":false,"v":"$12"}}]]
14:X
13:[["children",{"s":"__PAGE__","h":34,"d":{"r":["$","$2","c",{"children":[["$","p",null,{"children":"Your next task: pack order 1042."}],null,null]}],"p":false,"v":"$14"}}]]
c:[["children",{"s":"__PAGE__","h":162,"d":{"r":["$","$2","c",{"children":[["$","h1",null,{"children":"Today at the tea shop"}],null,["$","$Ld",null,{"children":["$","$9",null,{"name":"Next.MetadataOutlet","children":"$@e"}]}]]}],"p":false,"v":"$f"}}],["admin",{"s":"(__SLOT__)","h":66,"d":{"r":["$","$2","c",{"children":[null,["$","$L5",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L6",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","forbidden":"$undefined","unauthorized":"$undefined"}]]}],"p":false,"v":"$10"},"c":"$Q11"}],["staff",{"s":"(__SLOT__)","h":66,"d":{"r":["$","$2","c",{"children":[null,["$","$L5",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L6",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","forbidden":"$undefined","unauthorized":"$undefined"}]]}],"p":false,"v":"$10"},"c":"$Q13"}]]
8:[["children",{"s":"dashboard","h":98,"d":{"r":["$","$2","c",{"children":[null,["$","main",null,{"children":[["$","$L5",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L6",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","forbidden":"$undefined","unauthorized":"$undefined"}],["$","$9",null,{"fallback":["$","p",null,{"children":"Checking your role…"}],"children":"$La"}]]}]]}],"p":false,"v":"$b"},"c":"$Qc"}]]
1b:X
1c:X
1f:o87f,0:{"q":"","i":false,"S":true,"r":"$1","b":"cf5eAV0mV1PbLin4mGz4e","t":{"t":{"s":"","h":82,"d":{"r":["$","$2","c",{"children":[[["$","script","script-0",{"src":"/_next/static/chunks/3oh00mkbkq8u6.js","async":true,"nonce":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":[["$","nav",null,{"children":[["$","$L4",null,{"href":"/","children":"Home"}]," ",["$","$L4",null,{"href":"/dashboard","children":"Dashboard"}]]}],["$","$L5",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L6",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]]}]}]]}],"p":false,"v":"$7"},"c":"$Q8"},"h":{"r":["$","$2","h",{"children":[null,["$","$L16","B2_SjCFpbRWVcISSk4J8mv",{"children":"$L17"}],["$","$L19","B2_SjCFpbRWVcISSk4J8mm",{"children":[["$","div",null,{"hidden":true,"children":["$","$9",null,{"name":"Next.Metadata","children":"$L1a"}]}],null]}]]}],"p":false,"v":"$1b"}},"s":"$1c","a":"$@1d","w":"$@1e"}
1f:o8b,10:C
1c:300
17:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]
1f:o3c,e:null
1a:[["$","title","0",{"children":"Tea shop staff"}]]
1f:o126,a:["$","$L5",null,{"parallelRouterKey":"staff","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L6",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","forbidden":"$undefined","unauthorized":"$undefined"}]
1f:o13,1b:0
f:0
12:0
14:0
1f:o10,1e:true
1d:4945
1f:o24,1c:C
1b:C
1:C
f:C
12:C
14:C
b:C
7:C
1f:C
Try it yourself
Only the layout checks

Staff see their task, but the RSC payload they receive still holds Sales today: $1,240: Next.js rendered @admin for them too.

Both versions were built with next build from Next.js 16.4.3 and run with next start, which was asked for /dashboard as the owner, as staff, and for the RSC payload staff get. Only the admin slot's page changes. Reset goes back to the first version.

In the second version, app/dashboard/@admin/page.tsx asks for the role itself. Line 1 imports getRole from app/role.ts, one small module that reads the cookie, and line 4 returns null for anyone who isn't the owner. The payload a member of staff gets no longer holds Sales today. Each row is one version: what staff see, and whether the sales reach their browser.

What staff see, and what they receive
Where the role is checkedWhat staff seeDo they receive the sales?
Only the layout checks the roleYour next task: pack order 1042.Yes. The RSC payload holds Sales today: $1,240.
@admin/page.tsx checks it tooYour next task: pack order 1042.No. The admin slot rendered nothing for staff.
A server window renders two cards, the admin slot with Sales today and the staff slot with the next task. An arrow goes to a window called payload, for staff, which holds both cards. Another arrow goes to a window called screen, which shows only the staff card.server@admin: sales@staff: next taskpayload, for staff@admin: Sales today: $1,240@staff: pack order 1042screenNext task: pack order 1042.what staff receive is morethan what they see
The layout only chooses what shows. Next.js still rendered @admin for a member of staff, and sent its result in the RSC payload. Check the role inside the slot.

Keep the layout's choice for what the page looks like, and put the check next to the data. Next.js's docs suggest a data access layer for this: one module that checks who is asking before it returns any private data, used by every page and slot that needs it. Here, import { getRole } from "@/app/role"; is the start of one.

Planning the staff dashboard's files

When you add a slot, ask four questions about it. Each answer is a file in the slot's folder, or a line in its page:

  • What does it show at each address? A page.tsx for the addresses it has content for, and a default.tsx for every other address its siblings can show. The build checks this for you.
  • Does it wait for anything? A loading.tsx, so the shell has something to show in its place.
  • Can it fail? An error.tsx, so its failure stays in its own panel.
  • Who may see it? A check inside the slot's page, or in the data module it calls, not only in the layout.

Sort these moments in the dashboard's day by the file that handles them:

Which file handles it?
  • The stock slot, after a reload of /dashboard/late
  • The dashboard's own page, after a reload of /dashboard/late
  • The orders slot while the order service answers
  • A message in the page's shell for one slot
  • The stock slot when the stock service is down
  • A Try again button in the stock panel only
Try it yourself
0 of 6 correct

Sort each moment by the file in the slot's folder that decides what shows.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mix-ups

  • "A slot's folder is part of the address." No. /dashboard/orders and /dashboard/@orders answered 404, and @orders/late/page.tsx is /dashboard/late.
  • "children isn't a slot." It is, one without an @ folder. The build named it beside @stock when it had no default.tsx.
  • "A click and a reload show the same thing." Not with slots. The click kept the stock; the reload showed its default.tsx.
  • "Without default.tsx, a reload shows a 404." Not in Next.js 16.4.3: the build stops first. A default.tsx that calls notFound() gives the 404 on purpose.
  • "One failing panel breaks the whole dashboard." No. With an error.tsx in its slot, the stock failed and the orders still showed.
  • "A slot the layout doesn't return never runs." No. Next.js renders every slot and sends its result: the sales were in the staff's RSC payload.

Each row is one special file in a slot's folder: when it shows, and what it's for.

A slot's special files
FileWhen it showsWhat it's for
page.tsx in a slotWhen the address matches it.The slot's own content, like @orders/late/page.tsx for /dashboard/late.
default.tsxAfter a reload, when the slot has no page for the address.A fallback. In Next.js 16.4, every slot that can be unmatched needs one, children too.
loading.tsxWhile the slot's page waits.That slot's fallback, sent in the page's shell.
error.tsxWhen the slot's page throws.That slot's error screen. The other slots keep working.
layout.tsx in a slotAround every page of that slot.Parts the slot's pages share, like tabs.

Practice exercises

These exercises are about Next.js projects, which can't run inside this page. Answer first, then use the hints.

Exercise 1 · Warm-upFind a slot's address

At which address does app/dashboard/@stock/page.tsx show?

Starter codeText
app/
  dashboard/
    @orders/
      page.tsx
    @stock/
      page.tsx
    layout.tsx
    page.tsx
  layout.tsx
  page.tsx

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upAfter a click on Late orders

    A member of staff is on /dashboard and clicks Late orders. What does the stock slot show now?

    Starter codeText
    app/
      dashboard/
        @orders/
          late/
            page.tsx
          layout.tsx
          order-tabs.tsx
          page.tsx
        @stock/
          default.tsx
          page.tsx
        default.tsx
        layout.tsx
        page.tsx
      layout.tsx
      page.tsx

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeAfter a reload

      The same member of staff now reloads /dashboard/late. This is app/dashboard/@stock/default.tsx. What does the stock slot show?

      Starter codeTSX
      export default function StockDefault() {
        return <p>Stock is on the dashboard's first tab.</p>;
      }
      

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeFix the build

        With these files, next build stops: /dashboard/late is missing a matching page or default.tsx in children. Which file is missing?

        Starter codeText
        app/
          dashboard/
            @orders/
              late/
                page.tsx
              layout.tsx
              order-tabs.tsx
              page.tsx
            @stock/
              default.tsx
              page.tsx
            layout.tsx
            page.tsx
          layout.tsx
          page.tsx

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeThe same stock after a reload

          The staff want the stock on every dashboard address, reloaded or not. Replace this app/dashboard/@stock/default.tsx with one line.

          Starter codeTSX
          export default function StockDefault() {
            return <p>Stock is on the dashboard's first tab.</p>;
          }
          

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeFind the leak

            With this layout, a member of staff never sees the sales, but the sales still reach their browser. Which file should check the role, so that they don't?

            Starter codeTSX
            import { Suspense } from "react";
            import { cookies } from "next/headers";
            
            async function ForRole({ admin, staff }: { admin: React.ReactNode; staff: React.ReactNode }) {
              const role = (await cookies()).get("role")?.value;
              return role === "admin" ? admin : staff;
            }
            
            export default function DashboardLayout({ children, admin, staff }: LayoutProps<"/dashboard">) {
              return (
                <main>
                  {children}
                  <Suspense fallback={<p>Checking your role…</p>}>
                    <ForRole admin={admin} staff={staff} />
                  </Suspense>
                </main>
              );
            }
            

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              The parallel routes quiz · 7 questionsScore: first tries count
              1. Question 1 of 7Which address does app/dashboard/@orders/late/page.tsx make?

                Choose an answer to see the explanation.

              2. Question 2 of 7In this dashboard, which prop does app/dashboard/page.tsx fill?

                Read the code, then predictText
                app/
                  dashboard/
                    @orders/
                      page.tsx
                    @stock/
                      page.tsx
                    layout.tsx
                    page.tsx
                  layout.tsx
                  page.tsx

                Choose an answer to see the explanation.

              3. Question 3 of 7A visitor on /dashboard clicks Late orders. What does the stock slot show then?

                Choose an answer to see the explanation.

              4. Question 4 of 7With Next.js 16.4.3, the stock slot has no default.tsx and no page for /dashboard/late. What happens?

                Choose an answer to see the explanation.

              5. Question 5 of 7Both default.tsx files call notFound(). A visitor reloads /dashboard/late. What do they get?

                Choose an answer to see the explanation.

              6. Question 6 of 7The layout returns admin only when the role cookie is admin. Does a member of staff receive the admin slot's output?

                Choose an answer to see the explanation.

              7. Question 7 of 7Only the orders slot should show Loading orders… while it waits. Where does loading.tsx go?

                Choose an answer to see the explanation.

              Key takeaways

              • A folder named @something is a slot. Its pages render in the parent layout as the prop something, and its name never shows in the address.
              • children is a slot too, filled by the page.tsx beside the layout.
              • On a click, a slot with nothing for the new address keeps its last page. After a reload, it shows its default.tsx.
              • In Next.js 16.4, the build stops when a slot, children included, has neither a page nor a default.tsx for an address its siblings can show.
              • Each slot can have its own layout.tsx, loading.tsx and error.tsx. With Cache Components, the slots share one shell, and each streams in or fails on its own.
              • Next.js renders every slot and sends its result. Check who may see a slot inside the slot, not only in the layout.

              Remember the one-liner.
              An @ folder is one more page in the same layout: a click keeps it, a reload needs its default.tsx.

              Coming next: Intercepting routes and modals. It puts a slot to work for a modal: a tea opens on top of the list, and a reload shows the tea's full page.

              CompleteFrontend Clear concepts. Working examples.